mirror of
https://github.com/tutao/tutanota.git
synced 2025-10-19 16:03:43 +00:00
Inject type model resolvers
Passing instances explicitly avoids the situations where some of them might not be initialized. We also simplified the entity handling by converting entity updates to data with resolved types early so that the listening code doesn't have to deal with it. We did fix some of the bad test practices, e.g. setting/restoring env incorrectly. This matters now because accessors for type model initializers check env.mode. Co-authored-by: paw <paw-hub@users.noreply.github.com>
This commit is contained in:
parent
b2e5f83f89
commit
9e31ee0409
87 changed files with 1778 additions and 1452 deletions
76
libs/electron-updater.mjs
vendored
76
libs/electron-updater.mjs
vendored
|
@ -11603,47 +11603,55 @@ const coerce$1 = (version, options) => {
|
|||
};
|
||||
var coerce_1 = coerce$1;
|
||||
|
||||
class LRUCache {
|
||||
constructor () {
|
||||
this.max = 1000;
|
||||
this.map = new Map();
|
||||
}
|
||||
var lrucache;
|
||||
var hasRequiredLrucache;
|
||||
|
||||
get (key) {
|
||||
const value = this.map.get(key);
|
||||
if (value === undefined) {
|
||||
return undefined
|
||||
} else {
|
||||
// Remove the key from the map and add it to the end
|
||||
this.map.delete(key);
|
||||
this.map.set(key, value);
|
||||
return value
|
||||
}
|
||||
}
|
||||
function requireLrucache () {
|
||||
if (hasRequiredLrucache) return lrucache;
|
||||
hasRequiredLrucache = 1;
|
||||
class LRUCache {
|
||||
constructor () {
|
||||
this.max = 1000;
|
||||
this.map = new Map();
|
||||
}
|
||||
|
||||
delete (key) {
|
||||
return this.map.delete(key)
|
||||
}
|
||||
get (key) {
|
||||
const value = this.map.get(key);
|
||||
if (value === undefined) {
|
||||
return undefined
|
||||
} else {
|
||||
// Remove the key from the map and add it to the end
|
||||
this.map.delete(key);
|
||||
this.map.set(key, value);
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
set (key, value) {
|
||||
const deleted = this.delete(key);
|
||||
delete (key) {
|
||||
return this.map.delete(key)
|
||||
}
|
||||
|
||||
if (!deleted && value !== undefined) {
|
||||
// If cache is full, delete the least recently used item
|
||||
if (this.map.size >= this.max) {
|
||||
const firstKey = this.map.keys().next().value;
|
||||
this.delete(firstKey);
|
||||
}
|
||||
set (key, value) {
|
||||
const deleted = this.delete(key);
|
||||
|
||||
this.map.set(key, value);
|
||||
}
|
||||
if (!deleted && value !== undefined) {
|
||||
// If cache is full, delete the least recently used item
|
||||
if (this.map.size >= this.max) {
|
||||
const firstKey = this.map.keys().next().value;
|
||||
this.delete(firstKey);
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
this.map.set(key, value);
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
lrucache = LRUCache;
|
||||
return lrucache;
|
||||
}
|
||||
|
||||
var lrucache = LRUCache;
|
||||
|
||||
var range;
|
||||
var hasRequiredRange;
|
||||
|
||||
|
@ -11864,7 +11872,7 @@ function requireRange () {
|
|||
|
||||
range = Range;
|
||||
|
||||
const LRU = lrucache;
|
||||
const LRU = requireLrucache();
|
||||
const cache = new LRU();
|
||||
|
||||
const parseOptions = parseOptions_1;
|
||||
|
|
|
@ -2,7 +2,6 @@ import { CalendarSearchResultListEntry } from "./CalendarSearchListView.js"
|
|||
import { SearchRestriction, SearchResult } from "../../../../common/api/worker/search/SearchTypes.js"
|
||||
import { EntityEventsListener, EventController } from "../../../../common/api/main/EventController.js"
|
||||
import { CalendarEvent, CalendarEventTypeRef, ContactTypeRef, MailTypeRef } from "../../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { SomeEntity } from "../../../../common/api/common/EntityTypes.js"
|
||||
import { CLIENT_ONLY_CALENDARS, OperationType } from "../../../../common/api/common/TutanotaConstants.js"
|
||||
import { assertIsEntity2, elementIdPart, GENERATED_MAX_ID, getElementId, isSameId, ListElement } from "../../../../common/api/common/utils/EntityUtils.js"
|
||||
import { ListLoadingState, ListState } from "../../../../common/gui/base/List.js"
|
||||
|
@ -47,8 +46,7 @@ import { locator } from "../../../../common/api/main/CommonLocator.js"
|
|||
import { CalendarEventsRepository } from "../../../../common/calendar/date/CalendarEventsRepository"
|
||||
import { getClientOnlyCalendars } from "../../gui/CalendarGuiUtils"
|
||||
import { ListElementListModel } from "../../../../common/misc/ListElementListModel"
|
||||
import { AppName } from "@tutao/tutanota-utils/dist/TypeRef"
|
||||
import { resolveTypeRefFromAppAndTypeNameLegacy } from "../../../../common/api/common/EntityFunctions"
|
||||
import { TypeModelResolver } from "../../../../common/api/common/EntityFunctions"
|
||||
|
||||
const SEARCH_PAGE_SIZE = 100
|
||||
|
||||
|
@ -499,9 +497,7 @@ export class CalendarSearchViewModel {
|
|||
const { instanceListId, instanceId, operation } = update
|
||||
const id = [neverNull(instanceListId), instanceId] as const
|
||||
|
||||
const typeRef = update.typeId
|
||||
? new TypeRef<SomeEntity>(update.application as AppName, update.typeId)
|
||||
: resolveTypeRefFromAppAndTypeNameLegacy(update.application as AppName, update.type)
|
||||
const typeRef = update.typeRef
|
||||
|
||||
if (!this.isInSearchResult(typeRef, id) && isPossibleABirthdayContactUpdate) {
|
||||
return
|
||||
|
|
|
@ -113,6 +113,7 @@ import { MailImporter } from "../mail-app/mail/import/MailImporter.js"
|
|||
import { SyncTracker } from "../common/api/main/SyncTracker.js"
|
||||
import { KeyVerificationFacade } from "../common/api/worker/facades/lazy/KeyVerificationFacade"
|
||||
import { getEventWithDefaultTimes, setNextHalfHour } from "../common/api/common/utils/CommonCalendarUtils.js"
|
||||
import { ClientModelInfo, ClientTypeModelResolver } from "../common/api/common/EntityFunctions"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
|
@ -173,6 +174,10 @@ class CalendarLocator {
|
|||
private entropyFacade!: EntropyFacade
|
||||
private sqlCipherFacade!: SqlCipherFacade
|
||||
|
||||
readonly typeModelResolver: lazy<ClientTypeModelResolver> = lazyMemoized(() => {
|
||||
return ClientModelInfo.getInstance()
|
||||
})
|
||||
|
||||
readonly recipientsModel: lazyAsync<RecipientsModel> = lazyMemoized(async () => {
|
||||
const { RecipientsModel } = await import("../common/api/main/RecipientsModel.js")
|
||||
return new RecipientsModel(this.contactModel, this.logins, this.mailFacade, this.entityClient, this.keyVerificationFacade)
|
||||
|
@ -607,7 +612,7 @@ class CalendarLocator {
|
|||
this.progressTracker = new ProgressTracker()
|
||||
this.syncTracker = new SyncTracker()
|
||||
this.search = new CalendarSearchModel(() => this.calendarEventsRepository())
|
||||
this.entityClient = new EntityClient(restInterface)
|
||||
this.entityClient = new EntityClient(restInterface, this.typeModelResolver())
|
||||
this.cryptoFacade = cryptoFacade
|
||||
this.cacheStorage = cacheStorage
|
||||
this.entropyFacade = entropyFacade
|
||||
|
@ -638,6 +643,7 @@ class CalendarLocator {
|
|||
this.logins,
|
||||
this.eventController,
|
||||
() => this.usageTestController,
|
||||
this.typeModelResolver(),
|
||||
)
|
||||
this.usageTestController = new UsageTestController(this.usageTestModel)
|
||||
|
||||
|
|
|
@ -37,7 +37,6 @@ import type { BlobFacade } from "../../../common/api/worker/facades/lazy/BlobFac
|
|||
import { UserFacade } from "../../../common/api/worker/facades/UserFacade.js"
|
||||
import { OfflineStorage } from "../../../common/api/worker/offline/OfflineStorage.js"
|
||||
import { OFFLINE_STORAGE_MIGRATIONS, OfflineStorageMigrator } from "../../../common/api/worker/offline/OfflineStorageMigrator.js"
|
||||
import { globalServerModelInfo, resolveClientTypeReference, resolveServerTypeReference } from "../../../common/api/common/EntityFunctions.js"
|
||||
import { FileFacadeSendDispatcher } from "../../../common/native/common/generatedipc/FileFacadeSendDispatcher.js"
|
||||
import { NativePushFacadeSendDispatcher } from "../../../common/native/common/generatedipc/NativePushFacadeSendDispatcher.js"
|
||||
import { NativeCryptoFacadeSendDispatcher } from "../../../common/native/common/generatedipc/NativeCryptoFacadeSendDispatcher.js"
|
||||
|
@ -77,6 +76,8 @@ import { KeyAuthenticationFacade } from "../../../common/api/worker/facades/KeyA
|
|||
import { PublicKeyProvider } from "../../../common/api/worker/facades/PublicKeyProvider.js"
|
||||
import { InstancePipeline } from "../../../common/api/worker/crypto/InstancePipeline"
|
||||
import { ApplicationTypesFacade } from "../../../common/api/worker/facades/ApplicationTypesFacade"
|
||||
import { ClientModelInfo, ServerModelInfo, TypeModelResolver } from "../../../common/api/common/EntityFunctions"
|
||||
import { EphemeralCacheStorage } from "../../../common/api/worker/rest/EphemeralCacheStorage"
|
||||
|
||||
assertWorkerOrNode()
|
||||
|
||||
|
@ -154,15 +155,28 @@ export async function initLocator(worker: CalendarWorkerImpl, browserData: Brows
|
|||
|
||||
const suspensionHandler = new SuspensionHandler(mainInterface.infoMessageHandler, self)
|
||||
|
||||
locator.instancePipeline = new InstancePipeline(resolveClientTypeReference, resolveServerTypeReference)
|
||||
const clientModelInfo = ClientModelInfo.getInstance()
|
||||
const serverModelInfo = ServerModelInfo.getPossiblyUninitializedInstance(clientModelInfo)
|
||||
const typeModelResolver = new TypeModelResolver(clientModelInfo, serverModelInfo)
|
||||
locator.instancePipeline = new InstancePipeline(
|
||||
typeModelResolver.resolveClientTypeReference.bind(typeModelResolver),
|
||||
typeModelResolver.resolveServerTypeReference.bind(typeModelResolver),
|
||||
)
|
||||
locator.rsa = await createRsaImplementation(worker)
|
||||
|
||||
const domainConfig = new DomainConfigProvider().getCurrentDomainConfig()
|
||||
locator.restClient = new RestClient(suspensionHandler, domainConfig, () => locator.applicationTypesFacade)
|
||||
locator.serviceExecutor = new ServiceExecutor(locator.restClient, locator.user, locator.instancePipeline, () => locator.crypto)
|
||||
locator.serviceExecutor = new ServiceExecutor(locator.restClient, locator.user, locator.instancePipeline, () => locator.crypto, typeModelResolver)
|
||||
locator.entropyFacade = new EntropyFacade(locator.user, locator.serviceExecutor, random, () => locator.keyLoader)
|
||||
locator.blobAccessToken = new BlobAccessTokenFacade(locator.serviceExecutor, locator.user, dateProvider)
|
||||
const entityRestClient = new EntityRestClient(locator.user, locator.restClient, () => locator.crypto, locator.instancePipeline, locator.blobAccessToken)
|
||||
locator.blobAccessToken = new BlobAccessTokenFacade(locator.serviceExecutor, locator.user, dateProvider, typeModelResolver)
|
||||
const entityRestClient = new EntityRestClient(
|
||||
locator.user,
|
||||
locator.restClient,
|
||||
() => locator.crypto,
|
||||
locator.instancePipeline,
|
||||
locator.blobAccessToken,
|
||||
typeModelResolver,
|
||||
)
|
||||
locator.native = worker
|
||||
|
||||
locator.booking = lazyMemoized(async () => {
|
||||
|
@ -172,7 +186,7 @@ export async function initLocator(worker: CalendarWorkerImpl, browserData: Brows
|
|||
|
||||
const fileFacadeSendDispatcher = new FileFacadeSendDispatcher(worker)
|
||||
const fileApp = new NativeFileApp(fileFacadeSendDispatcher, new ExportFacadeSendDispatcher(worker))
|
||||
locator.applicationTypesFacade = await ApplicationTypesFacade.getInitialized(locator.restClient, fileFacadeSendDispatcher, globalServerModelInfo)
|
||||
locator.applicationTypesFacade = await ApplicationTypesFacade.getInitialized(locator.restClient, fileFacadeSendDispatcher, serverModelInfo)
|
||||
|
||||
let offlineStorageProvider
|
||||
if (isOfflineStorageAvailable()) {
|
||||
|
@ -185,6 +199,7 @@ export async function initLocator(worker: CalendarWorkerImpl, browserData: Brows
|
|||
new OfflineStorageMigrator(OFFLINE_STORAGE_MIGRATIONS),
|
||||
new CalendarOfflineCleaner(),
|
||||
locator.instancePipeline.modelMapper,
|
||||
typeModelResolver,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
@ -196,19 +211,19 @@ export async function initLocator(worker: CalendarWorkerImpl, browserData: Brows
|
|||
}
|
||||
|
||||
const maybeUninitializedStorage = new LateInitializedCacheStorageImpl(
|
||||
locator.instancePipeline.modelMapper,
|
||||
async (error: Error) => {
|
||||
await worker.sendError(error)
|
||||
},
|
||||
async () => new EphemeralCacheStorage(locator.instancePipeline.modelMapper, typeModelResolver),
|
||||
offlineStorageProvider,
|
||||
)
|
||||
|
||||
locator.cacheStorage = maybeUninitializedStorage
|
||||
|
||||
locator.cache = new DefaultEntityRestCache(entityRestClient, maybeUninitializedStorage)
|
||||
locator.cache = new DefaultEntityRestCache(entityRestClient, maybeUninitializedStorage, typeModelResolver)
|
||||
|
||||
locator.cachingEntityClient = new EntityClient(locator.cache)
|
||||
const nonCachingEntityClient = new EntityClient(entityRestClient)
|
||||
locator.cachingEntityClient = new EntityClient(locator.cache, typeModelResolver)
|
||||
const nonCachingEntityClient = new EntityClient(entityRestClient, typeModelResolver)
|
||||
|
||||
locator.cacheManagement = lazyMemoized(async () => {
|
||||
const { CacheManagementFacade } = await import("../../../common/api/worker/facades/lazy/CacheManagementFacade.js")
|
||||
|
@ -248,13 +263,14 @@ export async function initLocator(worker: CalendarWorkerImpl, browserData: Brows
|
|||
locator.restClient,
|
||||
locator.serviceExecutor,
|
||||
locator.instancePipeline,
|
||||
new OwnerEncSessionKeysUpdateQueue(locator.user, locator.serviceExecutor),
|
||||
new OwnerEncSessionKeysUpdateQueue(locator.user, locator.serviceExecutor, typeModelResolver),
|
||||
locator.cache as DefaultEntityRestCache,
|
||||
locator.keyLoader,
|
||||
asymmetricCrypto,
|
||||
locator.keyVerification,
|
||||
locator.publicKeyProvider,
|
||||
lazyMemoized(() => locator.keyRotation),
|
||||
typeModelResolver,
|
||||
)
|
||||
|
||||
locator.recoverCode = lazyMemoized(async () => {
|
||||
|
@ -330,7 +346,7 @@ export async function initLocator(worker: CalendarWorkerImpl, browserData: Brows
|
|||
/**
|
||||
* we don't want to try to use the cache in the login facade, because it may not be available (when no user is logged in)
|
||||
*/
|
||||
new EntityClient(locator.cache),
|
||||
new EntityClient(locator.cache, typeModelResolver),
|
||||
loginListener,
|
||||
locator.instancePipeline,
|
||||
locator.crypto,
|
||||
|
@ -347,6 +363,7 @@ export async function initLocator(worker: CalendarWorkerImpl, browserData: Brows
|
|||
await worker.sendError(error)
|
||||
},
|
||||
locator.cacheManagement,
|
||||
typeModelResolver,
|
||||
)
|
||||
|
||||
locator.userManagement = lazyMemoized(async () => {
|
||||
|
@ -417,6 +434,7 @@ export async function initLocator(worker: CalendarWorkerImpl, browserData: Brows
|
|||
locator.crypto,
|
||||
mainInterface.infoMessageHandler,
|
||||
locator.instancePipeline,
|
||||
locator.cachingEntityClient,
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -448,7 +466,7 @@ export async function initLocator(worker: CalendarWorkerImpl, browserData: Brows
|
|||
async (error: Error) => {
|
||||
await worker.sendError(error)
|
||||
},
|
||||
(queuedBatch: QueuedBatch[]) => noOp,
|
||||
noOp,
|
||||
)
|
||||
|
||||
locator.eventBusClient = new EventBusClient(
|
||||
|
@ -462,6 +480,7 @@ export async function initLocator(worker: CalendarWorkerImpl, browserData: Brows
|
|||
mainInterface.progressTracker,
|
||||
mainInterface.syncTracker,
|
||||
locator.applicationTypesFacade,
|
||||
typeModelResolver,
|
||||
)
|
||||
locator.login.init(locator.eventBusClient)
|
||||
locator.Const = Const
|
||||
|
@ -471,7 +490,7 @@ export async function initLocator(worker: CalendarWorkerImpl, browserData: Brows
|
|||
})
|
||||
locator.contactFacade = lazyMemoized(async () => {
|
||||
const { ContactFacade } = await import("../../../common/api/worker/facades/lazy/ContactFacade.js")
|
||||
return new ContactFacade(new EntityClient(locator.cache))
|
||||
return new ContactFacade(new EntityClient(locator.cache, typeModelResolver))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -20,15 +20,15 @@ import {
|
|||
} from "./utils/EntityUtils"
|
||||
import { Type, ValueType } from "./EntityConstants.js"
|
||||
import { downcast, groupByAndMap, last, promiseMap, TypeRef } from "@tutao/tutanota-utils"
|
||||
import { resolveClientTypeReference } from "./EntityFunctions"
|
||||
import type { ElementEntity, ListElementEntity, SomeEntity } from "./EntityTypes"
|
||||
import { NotAuthorizedError, NotFoundError } from "./error/RestError.js"
|
||||
import { ProgrammingError } from "./error/ProgrammingError"
|
||||
import { ClientTypeModelResolver } from "./EntityFunctions"
|
||||
|
||||
export class EntityClient {
|
||||
_target: EntityRestInterface
|
||||
|
||||
constructor(target: EntityRestInterface) {
|
||||
constructor(target: EntityRestInterface, private readonly typeModelResolver: ClientTypeModelResolver) {
|
||||
this._target = target
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,7 @@ export class EntityClient {
|
|||
}
|
||||
|
||||
async loadAll<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, start?: Id): Promise<T[]> {
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
|
||||
if (!start) {
|
||||
const _idValueId = Object.values(typeModel.values).find((valueType) => valueType.name === "_id")?.id
|
||||
|
@ -71,7 +71,7 @@ export class EntityClient {
|
|||
elements: T[]
|
||||
loadedCompletely: boolean
|
||||
}> {
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
if (typeModel.type !== Type.ListElement) throw new Error("only ListElement types are permitted")
|
||||
const loadedEntities = await this._target.loadRange<T>(typeRef, listId, start, rangeItemLimit, true)
|
||||
const filteredEntities = loadedEntities.filter((entity) => firstBiggerThanSecond(getElementId(entity), end, typeModel))
|
||||
|
@ -136,7 +136,7 @@ export class EntityClient {
|
|||
}
|
||||
|
||||
async loadRoot<T extends ElementEntity>(typeRef: TypeRef<T>, groupId: Id, opts: EntityRestClientLoadOptions = {}): Promise<T> {
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
const rootId = [groupId, typeModel.rootId] as const
|
||||
const root = await this.load<RootInstance>(RootInstanceTypeRef, rootId, opts)
|
||||
return this.load<T>(typeRef, downcast(root.reference), opts)
|
||||
|
|
|
@ -19,6 +19,7 @@ import usageModelInfo from "../entities/usage/ModelInfo.js"
|
|||
import { AppName, AppNameEnum } from "@tutao/tutanota-utils/dist/TypeRef"
|
||||
import { ProgrammingError } from "./error/ProgrammingError"
|
||||
import { AssociationType, Cardinality, Type, ValueType } from "./EntityConstants"
|
||||
import { isTest } from "./Env"
|
||||
|
||||
export const enum HttpMethod {
|
||||
GET = "GET",
|
||||
|
@ -50,6 +51,28 @@ export type ClientModels = {
|
|||
}
|
||||
|
||||
export class ClientModelInfo {
|
||||
private static instance: ClientModelInfo = new ClientModelInfo()
|
||||
|
||||
/**
|
||||
* Get an instance. AVOID using it, you should inject this instead.
|
||||
*/
|
||||
public static getInstance(): ClientModelInfo {
|
||||
return ClientModelInfo.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a fresh instance for tests. Will fail outside of tests. Reusing the same instance in tests leads to
|
||||
* corrupted state so better be safe and use a fresh one.
|
||||
*/
|
||||
public static getNewInstanceForTestsOnly(): ClientModelInfo {
|
||||
if (!isTest()) {
|
||||
throw new ProgrammingError()
|
||||
}
|
||||
return new ClientModelInfo()
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Model maps are needed for static analysis and dead-code elimination.
|
||||
* We access most types through the TypeRef but also sometimes we include them completely dynamically (e.g. encryption of aggregates).
|
||||
|
@ -88,7 +111,7 @@ export class ClientModelInfo {
|
|||
*
|
||||
* @param typeRef the typeRef for which we will return the typeModel.
|
||||
*/
|
||||
public async resolveTypeReference(typeRef: TypeRef<any>): Promise<ClientTypeModel> {
|
||||
public async resolveClientTypeReference(typeRef: TypeRef<any>): Promise<ClientTypeModel> {
|
||||
const typeModel = this.typeModels[typeRef.app][typeRef.typeId]
|
||||
if (typeModel == null) {
|
||||
throw new Error("Cannot find TypeRef: " + JSON.stringify(typeRef))
|
||||
|
@ -117,12 +140,32 @@ export class ServerModelInfo {
|
|||
private applicationTypesHash: ApplicationTypesHash | null = null
|
||||
public typeModels: ServerModels | null = null
|
||||
|
||||
constructor(private readonly clientModelInfo: ClientModelInfo) {}
|
||||
private static instance: ServerModelInfo | null
|
||||
|
||||
public getApplicationTypesHash(): ApplicationTypesHash | null {
|
||||
return this.applicationTypesHash
|
||||
/**
|
||||
* Get an instance. Might or might not be initialized.
|
||||
* AVOID using it, you should inject this instead.
|
||||
*/
|
||||
public static getPossiblyUninitializedInstance(clientModelInfo: ClientModelInfo): ServerModelInfo {
|
||||
if (ServerModelInfo.instance == null) {
|
||||
ServerModelInfo.instance = new ServerModelInfo(clientModelInfo)
|
||||
}
|
||||
return ServerModelInfo.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a fresh, uninitialized instance, for tests only.
|
||||
* @param clientModelInfo
|
||||
*/
|
||||
public static getUninitializedInstanceForTestsOnly(clientModelInfo: ClientModelInfo): ServerModelInfo {
|
||||
if (!isTest()) {
|
||||
throw new ProgrammingError()
|
||||
}
|
||||
return new ServerModelInfo(clientModelInfo)
|
||||
}
|
||||
|
||||
private constructor(private readonly clientModelInfo: ClientModelInfo) {}
|
||||
|
||||
public init(newApplicationTypesHash: ApplicationTypesHash, parsedApplicationTypesJson: Record<string, any>) {
|
||||
let newTypeModels = {} as ServerModels
|
||||
for (const appName of Object.values(AppNameEnum)) {
|
||||
|
@ -134,6 +177,10 @@ export class ServerModelInfo {
|
|||
this.applicationTypesHash = newApplicationTypesHash
|
||||
}
|
||||
|
||||
public getApplicationTypesHash(): ApplicationTypesHash | null {
|
||||
return this.applicationTypesHash
|
||||
}
|
||||
|
||||
private parseAllTypesForModel(modelInfo: Record<string, unknown>): {
|
||||
types: Record<string, ServerTypeModel>
|
||||
version: number
|
||||
|
@ -255,7 +302,7 @@ export class ServerModelInfo {
|
|||
else throw new Error(`value: ${value} is not boolean compatible`)
|
||||
}
|
||||
|
||||
public async resolveTypeReference(typeRef: TypeRef<any>): Promise<ServerTypeModel> {
|
||||
public async resolveServerTypeReference(typeRef: TypeRef<any>): Promise<ServerTypeModel> {
|
||||
if (this.typeModels == null) {
|
||||
throw new ProgrammingError("Tried to resolve server type ref before initialization. Call ensure_latest_server_model first?")
|
||||
}
|
||||
|
@ -285,13 +332,33 @@ export function _verifyType(typeModel: ClientTypeModel) {
|
|||
}
|
||||
}
|
||||
|
||||
// @singleton global client and server model info to always use the same and up-to-date typeModels
|
||||
export const globalClientModelInfo = new ClientModelInfo()
|
||||
export const globalServerModelInfo = new ServerModelInfo(globalClientModelInfo)
|
||||
export interface ClientTypeModelResolver {
|
||||
resolveClientTypeReference(typeRef: TypeRef<any>): Promise<ClientTypeModel>
|
||||
|
||||
export const resolveClientTypeReference = (typeRef: TypeRef<any>) => globalClientModelInfo.resolveTypeReference(typeRef)
|
||||
export const resolveServerTypeReference = (typeRef: TypeRef<any>) => globalServerModelInfo.resolveTypeReference(typeRef)
|
||||
|
||||
export const resolveTypeRefFromAppAndTypeNameLegacy = (app: AppName, typeName: string): TypeRef<any> => {
|
||||
return globalClientModelInfo.resolveTypeRefFromAppAndTypeNameLegacy(app, typeName)
|
||||
resolveTypeRefFromAppAndTypeNameLegacy(app: AppName, typeName: string): TypeRef<any>
|
||||
}
|
||||
|
||||
export interface ServerTypeModelResolver {
|
||||
resolveServerTypeReference(typeRef: TypeRef<any>): Promise<ServerTypeModel>
|
||||
getServerApplicationTypesModelHash(): ApplicationTypesHash | null
|
||||
}
|
||||
|
||||
export class TypeModelResolver implements ClientTypeModelResolver, ServerTypeModelResolver {
|
||||
constructor(private readonly clientModelInfo: ClientModelInfo, private readonly serverModelInfo: ServerModelInfo) {}
|
||||
|
||||
resolveClientTypeReference(typeRef: TypeRef<any>): Promise<ClientTypeModel> {
|
||||
return this.clientModelInfo.resolveClientTypeReference(typeRef)
|
||||
}
|
||||
|
||||
resolveServerTypeReference(typeRef: TypeRef<any>): Promise<ServerTypeModel> {
|
||||
return this.serverModelInfo.resolveServerTypeReference(typeRef)
|
||||
}
|
||||
|
||||
resolveTypeRefFromAppAndTypeNameLegacy(app: AppName, typeName: string): TypeRef<any> {
|
||||
return this.clientModelInfo.resolveTypeRefFromAppAndTypeNameLegacy(app, typeName)
|
||||
}
|
||||
|
||||
getServerApplicationTypesModelHash(): ApplicationTypesHash | null {
|
||||
return this.serverModelInfo.getApplicationTypesHash()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,41 @@
|
|||
import { OperationType } from "../TutanotaConstants.js"
|
||||
import { EntityUpdate } from "../../entities/sys/TypeRefs.js"
|
||||
import { SomeEntity } from "../EntityTypes.js"
|
||||
import { AppName, isSameTypeRefByAttr, TypeRef } from "@tutao/tutanota-utils"
|
||||
import { AppName, isSameTypeRef, isSameTypeRefByAttr, TypeRef } from "@tutao/tutanota-utils"
|
||||
import { isSameId } from "./EntityUtils.js"
|
||||
import { resolveTypeRefFromAppAndTypeNameLegacy } from "../EntityFunctions"
|
||||
import { ClientTypeModelResolver } from "../EntityFunctions"
|
||||
|
||||
/**
|
||||
* A type similar to {@link EntityUpdate} but mapped to make it easier to work with.
|
||||
*/
|
||||
export type EntityUpdateData = {
|
||||
application: string
|
||||
typeId: number | null
|
||||
type: string
|
||||
typeRef: TypeRef<any>
|
||||
instanceListId: string
|
||||
instanceId: string
|
||||
operation: OperationType
|
||||
}
|
||||
|
||||
export function entityUpdateToUpdateData(update: EntityUpdate): EntityUpdateData {
|
||||
export async function entityUpdateToUpdateData(clientTypeModelResolver: ClientTypeModelResolver, update: EntityUpdate): Promise<EntityUpdateData> {
|
||||
const typeId = update.typeId ? parseInt(update.typeId) : null
|
||||
const typeIdOfEntityUpdateType = typeId
|
||||
? new TypeRef<SomeEntity>(update.application as AppName, typeId)
|
||||
: clientTypeModelResolver.resolveTypeRefFromAppAndTypeNameLegacy(update.application as AppName, update.type)
|
||||
return {
|
||||
application: update.application,
|
||||
typeId: update.typeId ? parseInt(update.typeId) : null,
|
||||
type: update.type,
|
||||
typeRef: typeIdOfEntityUpdateType,
|
||||
instanceListId: update.instanceListId,
|
||||
instanceId: update.instanceId,
|
||||
operation: update.operation as OperationType,
|
||||
}
|
||||
}
|
||||
|
||||
export function isUpdateForTypeRef(typeRef: TypeRef<unknown>, update: EntityUpdateData | EntityUpdate): boolean {
|
||||
const typeId = typeof update.typeId === "number" ? update.typeId : update.typeId ? parseInt(update.typeId) : null
|
||||
const typeIdOfEntityUpdateType = typeId ? typeId : resolveTypeRefFromAppAndTypeNameLegacy(update.application as AppName, update.type).typeId
|
||||
return isSameTypeRefByAttr(typeRef, update.application, typeIdOfEntityUpdateType)
|
||||
export function isUpdateForTypeRef(typeRef: TypeRef<unknown>, update: EntityUpdateData): boolean {
|
||||
return isSameTypeRef(typeRef, update.typeRef)
|
||||
}
|
||||
|
||||
export function isUpdateFor<T extends SomeEntity>(entity: T, update: EntityUpdateData): boolean {
|
||||
const typeRef = entity._type as TypeRef<T>
|
||||
return (
|
||||
isUpdateForTypeRef(typeRef, update) &&
|
||||
isSameTypeRef(typeRef, update.typeRef) &&
|
||||
(update.instanceListId === "" ? isSameId(update.instanceId, entity._id) : isSameId([update.instanceListId, update.instanceId], entity._id))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -40,25 +40,19 @@ export class EventController {
|
|||
return this.countersStream.map(identity)
|
||||
}
|
||||
|
||||
async onEntityUpdateReceived(entityUpdates: ReadonlyArray<EntityUpdate>, eventOwnerGroupId: Id): Promise<void> {
|
||||
async onEntityUpdateReceived(entityUpdates: ReadonlyArray<EntityUpdateData>, eventOwnerGroupId: Id): Promise<void> {
|
||||
let loginsUpdates = Promise.resolve()
|
||||
|
||||
if (this.logins.isUserLoggedIn()) {
|
||||
// the UserController must be notified first as other event receivers depend on it to be up-to-date
|
||||
loginsUpdates = this.logins.getUserController().entityEventsReceived(
|
||||
entityUpdates.map((e) => entityUpdateToUpdateData(e)),
|
||||
eventOwnerGroupId,
|
||||
)
|
||||
loginsUpdates = this.logins.getUserController().entityEventsReceived(entityUpdates, eventOwnerGroupId)
|
||||
}
|
||||
|
||||
return loginsUpdates
|
||||
.then(async () => {
|
||||
// sequentially to prevent parallel loading of instances
|
||||
for (const listener of this.entityListeners) {
|
||||
await listener(
|
||||
entityUpdates.map((e) => entityUpdateToUpdateData(e)),
|
||||
eventOwnerGroupId,
|
||||
)
|
||||
await listener(entityUpdates, eventOwnerGroupId)
|
||||
}
|
||||
})
|
||||
.then(noOp)
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
WebsocketLeaderStatus,
|
||||
WebsocketLeaderStatusTypeRef,
|
||||
} from "../entities/sys/TypeRefs.js"
|
||||
import { binarySearch, delay, identity, lastThrow, ofClass, randomIntFromInterval, TypeRef } from "@tutao/tutanota-utils"
|
||||
import { binarySearch, delay, identity, lastThrow, ofClass, promiseMap, randomIntFromInterval, TypeRef } from "@tutao/tutanota-utils"
|
||||
import { OutOfSyncError } from "../common/error/OutOfSyncError"
|
||||
import { CloseEventBusOption, GroupType, SECOND_MS } from "../common/TutanotaConstants"
|
||||
import { CancelledError } from "../common/error/CancelledError"
|
||||
|
@ -33,7 +33,7 @@ import { EntityRestCache } from "./rest/DefaultEntityRestCache.js"
|
|||
import { SleepDetector } from "./utils/SleepDetector.js"
|
||||
import sysModelInfo from "../entities/sys/ModelInfo.js"
|
||||
import tutanotaModelInfo from "../entities/tutanota/ModelInfo.js"
|
||||
import { ApplicationTypesHash, globalServerModelInfo } from "../common/EntityFunctions.js"
|
||||
import { ApplicationTypesHash, TypeModelResolver } from "../common/EntityFunctions.js"
|
||||
import { PhishingMarkerWebsocketDataTypeRef, ReportedMailFieldMarker } from "../entities/tutanota/TypeRefs"
|
||||
import { UserFacade } from "./facades/UserFacade"
|
||||
import { ExposedProgressTracker } from "../main/ProgressTracker.js"
|
||||
|
@ -41,6 +41,7 @@ import { SyncTracker } from "../main/SyncTracker.js"
|
|||
import { Entity, ServerModelUntypedInstance } from "../common/EntityTypes"
|
||||
import { InstancePipeline } from "./crypto/InstancePipeline"
|
||||
import { ApplicationTypesFacade } from "./facades/ApplicationTypesFacade"
|
||||
import { EntityUpdateData, entityUpdateToUpdateData } from "../common/utils/EntityUpdateUtils"
|
||||
|
||||
assertWorkerOrNode()
|
||||
|
||||
|
@ -91,7 +92,7 @@ export interface EventBusListener {
|
|||
|
||||
onLeaderStatusChanged(leaderStatus: WebsocketLeaderStatus): unknown
|
||||
|
||||
onEntityEventsReceived(events: EntityUpdate[], batchId: Id, groupId: Id): Promise<void>
|
||||
onEntityEventsReceived(events: readonly EntityUpdateData[], batchId: Id, groupId: Id): Promise<void>
|
||||
|
||||
/**
|
||||
* @param markers only phishing (not spam) markers will be sent as event bus updates
|
||||
|
@ -153,6 +154,7 @@ export class EventBusClient {
|
|||
private readonly progressTracker: ExposedProgressTracker,
|
||||
private readonly syncTracker: SyncTracker,
|
||||
private readonly applicationTypesFacade: ApplicationTypesFacade,
|
||||
private readonly typeModelResolver: TypeModelResolver,
|
||||
) {
|
||||
// We are not connected by default and will not try to unless connect() is called
|
||||
this.state = EventBusState.Terminated
|
||||
|
@ -298,8 +300,8 @@ export class EventBusClient {
|
|||
case MessageType.EntityUpdate: {
|
||||
const entityUpdateData = await this.decodeEntityEventValue(WebsocketEntityDataTypeRef, JSON.parse(value))
|
||||
await this.updateServerModelIfNeeded(entityUpdateData.applicationTypesHash)
|
||||
|
||||
this.entityUpdateMessageQueue.add(entityUpdateData.eventBatchId, entityUpdateData.eventBatchOwner, entityUpdateData.entityUpdates)
|
||||
const updates = await promiseMap(entityUpdateData.entityUpdates, (event) => entityUpdateToUpdateData(this.typeModelResolver, event))
|
||||
this.entityUpdateMessageQueue.add(entityUpdateData.eventBatchId, entityUpdateData.eventBatchOwner, updates)
|
||||
break
|
||||
}
|
||||
case MessageType.UnreadCounterUpdate: {
|
||||
|
@ -335,7 +337,7 @@ export class EventBusClient {
|
|||
|
||||
private async updateServerModelIfNeeded(applicationTypesHash: ApplicationTypesHash) {
|
||||
// handle new server model and update the applicationTypesJson file if applicable
|
||||
if (applicationTypesHash !== globalServerModelInfo.getApplicationTypesHash()) {
|
||||
if (applicationTypesHash !== this.typeModelResolver.getServerApplicationTypesModelHash()) {
|
||||
await this.applicationTypesFacade.getServerApplicationTypesJson()
|
||||
}
|
||||
}
|
||||
|
@ -526,7 +528,8 @@ export class EventBusClient {
|
|||
// Count all batches that will actually be processed so that the progress is correct
|
||||
let totalExpectedBatches = 0
|
||||
for (const batch of timeSortedEventBatches) {
|
||||
const batchWasAddedToQueue = this.addBatch(getElementId(batch), getListId(batch), batch.events, eventQueue)
|
||||
const updates = await promiseMap(batch.events, (event) => entityUpdateToUpdateData(this.typeModelResolver, event))
|
||||
const batchWasAddedToQueue = this.addBatch(getElementId(batch), getListId(batch), updates, eventQueue)
|
||||
if (batchWasAddedToQueue) {
|
||||
// Set as last only if it was inserted with success
|
||||
this.lastInitialEventBatch = getElementId(batch)
|
||||
|
@ -640,7 +643,7 @@ export class EventBusClient {
|
|||
}
|
||||
}
|
||||
|
||||
private addBatch(batchId: Id, groupId: Id, events: ReadonlyArray<EntityUpdate>, eventQueue: EventQueue): boolean {
|
||||
private addBatch(batchId: Id, groupId: Id, events: ReadonlyArray<EntityUpdateData>, eventQueue: EventQueue): boolean {
|
||||
const lastForGroup = this.lastEntityEventIds.get(groupId) || []
|
||||
// find the position for inserting into last entity events (negative value is considered as not present in the array)
|
||||
const index = binarySearch(lastForGroup, batchId, compareOldestFirst)
|
||||
|
@ -669,7 +672,7 @@ export class EventBusClient {
|
|||
private async processEventBatch(batch: QueuedBatch): Promise<void> {
|
||||
try {
|
||||
if (this.isTerminated()) return
|
||||
const filteredEvents = await this.cache.entityEventsReceived(batch)
|
||||
const filteredEvents = await this.cache.entityEventsReceived(batch.events, batch.batchId, batch.groupId)
|
||||
if (!this.isTerminated()) await this.listener.onEntityEventsReceived(filteredEvents, batch.batchId, batch.groupId)
|
||||
|
||||
if (batch.batchId === this.lastInitialEventBatch) {
|
||||
|
|
|
@ -22,7 +22,7 @@ import { ConfigurationDatabase } from "./facades/lazy/ConfigurationDatabase.js"
|
|||
import { KeyRotationFacade } from "./facades/KeyRotationFacade.js"
|
||||
import { CacheManagementFacade } from "./facades/lazy/CacheManagementFacade.js"
|
||||
import type { QueuedBatch } from "./EventQueue.js"
|
||||
import { isUpdateForTypeRef } from "../common/utils/EntityUpdateUtils"
|
||||
import { EntityUpdateData, isUpdateForTypeRef } from "../common/utils/EntityUpdateUtils"
|
||||
|
||||
/** A bit of glue to distribute event bus events across the app. */
|
||||
export class EventBusEventCoordinator implements EventBusListener {
|
||||
|
@ -36,24 +36,23 @@ export class EventBusEventCoordinator implements EventBusListener {
|
|||
private readonly keyRotationFacade: KeyRotationFacade,
|
||||
private readonly cacheManagementFacade: lazyAsync<CacheManagementFacade>,
|
||||
private readonly sendError: (error: Error) => Promise<void>,
|
||||
private readonly appSpecificBatchHandling: (queuedBatch: QueuedBatch[]) => void,
|
||||
private readonly appSpecificBatchHandling: (events: readonly EntityUpdateData[], batchId: Id, groupId: Id) => void,
|
||||
) {}
|
||||
|
||||
onWebsocketStateChanged(state: WsConnectionState) {
|
||||
this.connectivityListener.updateWebSocketState(state)
|
||||
}
|
||||
|
||||
async onEntityEventsReceived(events: EntityUpdate[], batchId: Id, groupId: Id): Promise<void> {
|
||||
async onEntityEventsReceived(events: readonly EntityUpdateData[], batchId: Id, groupId: Id): Promise<void> {
|
||||
await this.entityEventsReceived(events)
|
||||
await (await this.mailFacade()).entityEventsReceived(events)
|
||||
await this.eventController.onEntityUpdateReceived(events, groupId)
|
||||
// Call the indexer in this last step because now the processed event is stored and the indexer has a separate event queue that
|
||||
// shall not receive the event twice.
|
||||
if (!isTest() && !isAdminClient()) {
|
||||
const queuedBatch = { groupId, batchId, events }
|
||||
const configurationDatabase = await this.configurationDatabase()
|
||||
await configurationDatabase.onEntityEventsReceived(queuedBatch)
|
||||
this.appSpecificBatchHandling([queuedBatch])
|
||||
await configurationDatabase.onEntityEventsReceived(events, batchId, groupId)
|
||||
this.appSpecificBatchHandling(events, batchId, groupId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,7 +83,7 @@ export class EventBusEventCoordinator implements EventBusListener {
|
|||
this.eventController.onCountersUpdateReceived(counter)
|
||||
}
|
||||
|
||||
private async entityEventsReceived(data: EntityUpdate[]): Promise<void> {
|
||||
private async entityEventsReceived(data: readonly EntityUpdateData[]): Promise<void> {
|
||||
// This is a compromise to not add entityClient to UserFacade which would introduce a circular dep.
|
||||
const groupKeyUpdates: IdTuple[] = [] // GroupKeyUpdates all in the same list
|
||||
const user = this.userFacade.getUser()
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { OperationType } from "../common/TutanotaConstants.js"
|
||||
import { findAllAndRemove } from "@tutao/tutanota-utils"
|
||||
import { findAllAndRemove, isSameTypeRef } from "@tutao/tutanota-utils"
|
||||
import { ConnectionError, ServiceUnavailableError } from "../common/error/RestError.js"
|
||||
import type { EntityUpdate } from "../entities/sys/TypeRefs.js"
|
||||
import { ProgrammingError } from "../common/error/ProgrammingError.js"
|
||||
import { ProgressMonitorDelegate } from "./ProgressMonitorDelegate.js"
|
||||
import { EntityUpdateData } from "../common/utils/EntityUpdateUtils"
|
||||
|
||||
export type QueuedBatch = {
|
||||
events: EntityUpdate[]
|
||||
events: EntityUpdateData[]
|
||||
groupId: Id
|
||||
batchId: Id
|
||||
}
|
||||
|
@ -24,13 +24,12 @@ type QueueAction = (nextElement: QueuedBatch) => Promise<void>
|
|||
* @param batch entity updates of the batch.
|
||||
* @private visibleForTests
|
||||
*/
|
||||
export function batchMod(batchId: Id, batch: ReadonlyArray<EntityUpdate>, entityUpdate: EntityUpdate): EntityModificationType {
|
||||
export function batchMod(batchId: Id, batch: ReadonlyArray<EntityUpdateData>, entityUpdate: EntityUpdateData): EntityModificationType {
|
||||
for (const batchEvent of batch) {
|
||||
if (
|
||||
entityUpdate.instanceId === batchEvent.instanceId &&
|
||||
entityUpdate.instanceListId === batchEvent.instanceListId &&
|
||||
entityUpdate.application === batchEvent.application &&
|
||||
entityUpdate.typeId === batchEvent.typeId
|
||||
isSameTypeRef(entityUpdate.typeRef, batchEvent.typeRef)
|
||||
) {
|
||||
switch (batchEvent.operation) {
|
||||
case OperationType.CREATE:
|
||||
|
@ -49,7 +48,7 @@ export function batchMod(batchId: Id, batch: ReadonlyArray<EntityUpdate>, entity
|
|||
}
|
||||
|
||||
throw new ProgrammingError(
|
||||
`Batch does not have events for ${entityUpdate.application}/${entityUpdate.typeId} ${lastOperationKey(entityUpdate)}, batchId: ${batchId}`,
|
||||
`Batch does not have events for ${entityUpdate.typeRef.app}/${entityUpdate.typeRef.typeId} ${lastOperationKey(entityUpdate)}, batchId: ${batchId}`,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -58,8 +57,8 @@ export function batchMod(batchId: Id, batch: ReadonlyArray<EntityUpdate>, entity
|
|||
// Adding brand for type safety.
|
||||
type LastOperationKey = string & { __brand: "lastOpeKey" }
|
||||
|
||||
function lastOperationKey(update: EntityUpdate): LastOperationKey {
|
||||
const typeIdentifier = `${update.application}/${update.typeId}`
|
||||
function lastOperationKey(update: EntityUpdateData): LastOperationKey {
|
||||
const typeIdentifier = `${update.typeRef.app}/${update.typeRef.typeId}`
|
||||
if (update.instanceListId) {
|
||||
return `${typeIdentifier}/${update.instanceListId}/${update.instanceId}` as LastOperationKey
|
||||
} else {
|
||||
|
@ -103,7 +102,7 @@ export class EventQueue {
|
|||
/**
|
||||
* @return whether the batch was added (not optimized away)
|
||||
*/
|
||||
add(batchId: Id, groupId: Id, newEvents: ReadonlyArray<EntityUpdate>): boolean {
|
||||
add(batchId: Id, groupId: Id, newEvents: ReadonlyArray<EntityUpdateData>): boolean {
|
||||
const newBatch: QueuedBatch = {
|
||||
events: [],
|
||||
groupId,
|
||||
|
@ -129,7 +128,7 @@ export class EventQueue {
|
|||
return newBatch.events.length > 0
|
||||
}
|
||||
|
||||
private optimizingAddEvents(newBatch: QueuedBatch, batchId: Id, groupId: Id, newEvents: ReadonlyArray<EntityUpdate>): void {
|
||||
private optimizingAddEvents(newBatch: QueuedBatch, batchId: Id, groupId: Id, newEvents: ReadonlyArray<EntityUpdateData>): void {
|
||||
for (const newEvent of newEvents) {
|
||||
const lastOpKey = lastOperationKey(newEvent)
|
||||
const lastBatchForEntity = this.lastOperationForEntity.get(lastOpKey)
|
||||
|
@ -157,7 +156,7 @@ export class EventQueue {
|
|||
|
||||
case EntityModificationType.DELETE:
|
||||
throw new ProgrammingError(
|
||||
`UPDATE not allowed after DELETE. Last batch: ${lastBatchForEntity.batchId}, new batch: ${batchId}, ${newEvent.typeId} ${lastOpKey}`,
|
||||
`UPDATE not allowed after DELETE. Last batch: ${lastBatchForEntity.batchId}, new batch: ${batchId}, ${newEvent.typeRef.typeId} ${lastOpKey}`,
|
||||
)
|
||||
}
|
||||
} else if (newEntityModification === EntityModificationType.DELETE) {
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
PublicKeyIdentifierType,
|
||||
SYSTEM_GROUP_MAIL_ADDRESS,
|
||||
} from "../../common/TutanotaConstants"
|
||||
import { HttpMethod, resolveClientTypeReference, resolveServerTypeReference } from "../../common/EntityFunctions"
|
||||
import { HttpMethod, TypeModelResolver } from "../../common/EntityFunctions"
|
||||
import type { BucketPermission, GroupMembership, InstanceSessionKey, Permission } from "../../entities/sys/TypeRefs.js"
|
||||
import {
|
||||
BucketPermissionTypeRef,
|
||||
|
@ -37,8 +37,6 @@ import {
|
|||
PushIdentifierTypeRef,
|
||||
} from "../../entities/sys/TypeRefs.js"
|
||||
import {
|
||||
Contact,
|
||||
ContactTypeRef,
|
||||
createEncryptTutanotaPropertiesData,
|
||||
createInternalRecipientKeyData,
|
||||
createSymEncInternalRecipientKeyData,
|
||||
|
@ -50,9 +48,8 @@ import {
|
|||
SymEncInternalRecipientKeyData,
|
||||
TutanotaPropertiesTypeRef,
|
||||
} from "../../entities/tutanota/TypeRefs.js"
|
||||
import { LockedError, NotFoundError, PayloadTooLargeError, TooManyRequestsError } from "../../common/error/RestError"
|
||||
import { NotFoundError, PayloadTooLargeError, TooManyRequestsError } from "../../common/error/RestError"
|
||||
import { SessionKeyNotFoundError } from "../../common/error/SessionKeyNotFoundError"
|
||||
import { birthdayToIsoDate, oldBirthdayToBirthday } from "../../common/utils/BirthdayUtils"
|
||||
import type { ClientModelEncryptedParsedInstance, ClientTypeModel, Entity, ServerModelEncryptedParsedInstance, SomeEntity } from "../../common/EntityTypes"
|
||||
import { assertWorkerOrNode } from "../../common/Env"
|
||||
import type { EntityClient } from "../../common/EntityClient"
|
||||
|
@ -87,9 +84,9 @@ import type { KeyVerificationFacade } from "../facades/lazy/KeyVerificationFacad
|
|||
import { PublicKeyProvider } from "../facades/PublicKeyProvider.js"
|
||||
import { KeyVersion, Nullable } from "@tutao/tutanota-utils/dist/Utils.js"
|
||||
import { KeyRotationFacade } from "../facades/KeyRotationFacade.js"
|
||||
import { typeRefToRestPath } from "../rest/EntityRestClient"
|
||||
import { InstancePipeline } from "./InstancePipeline"
|
||||
import { EntityAdapter } from "./EntityAdapter"
|
||||
import { typeModelToRestPath } from "../rest/EntityRestClient"
|
||||
|
||||
assertWorkerOrNode()
|
||||
|
||||
|
@ -112,6 +109,7 @@ export class CryptoFacade {
|
|||
private readonly lazyKeyVerificationFacade: lazyAsync<KeyVerificationFacade>,
|
||||
private readonly publicKeyProvider: PublicKeyProvider,
|
||||
private readonly keyRotationFacade: lazy<KeyRotationFacade>,
|
||||
private readonly typeModelResolver: TypeModelResolver,
|
||||
) {}
|
||||
|
||||
/** Resolve a session key an {@param instance} using an already known {@param ownerKey}. */
|
||||
|
@ -133,7 +131,7 @@ export class CryptoFacade {
|
|||
* @param instance The unencrypted (client-side) instance or encrypted (server-side) object literal
|
||||
*/
|
||||
async resolveSessionKey(instance: Entity): Promise<Nullable<AesKey>> {
|
||||
const clientTypeModel = await resolveClientTypeReference(instance._type)
|
||||
const clientTypeModel = await this.typeModelResolver.resolveClientTypeReference(instance._type)
|
||||
if (!clientTypeModel.encrypted) {
|
||||
return null
|
||||
}
|
||||
|
@ -186,8 +184,13 @@ export class CryptoFacade {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves session keys using the bucket key on the instance.
|
||||
* @param instance with a set bucketKey
|
||||
* @throws {Error} if `instance.bucketKey == null`
|
||||
*/
|
||||
public async resolveWithBucketKey(instance: Entity): Promise<ResolvedSessionKeys> {
|
||||
const typeModel = await resolveClientTypeReference(instance._type)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(instance._type)
|
||||
const bucketKey = assertNotNull(instance.bucketKey)
|
||||
|
||||
let decryptedBucketKey: AesKey
|
||||
|
@ -468,7 +471,7 @@ export class CryptoFacade {
|
|||
if (decryptedInstance.isAdapter) {
|
||||
const entityAdapter = downcast<EntityAdapter>(instance)
|
||||
const parsedInstance = await this.instancePipeline.cryptoMapper.decryptParsedInstance(
|
||||
await resolveServerTypeReference(instance._type),
|
||||
await this.typeModelResolver.resolveServerTypeReference(instance._type),
|
||||
entityAdapter.encryptedParsedInstance as ServerModelEncryptedParsedInstance,
|
||||
resolvedSessionKeyForInstance,
|
||||
)
|
||||
|
@ -774,7 +777,8 @@ export class CryptoFacade {
|
|||
this.setOwnerEncSessionKey(instance, newOwnerEncSessionKey)
|
||||
|
||||
const id = instance._id
|
||||
const path = (await typeRefToRestPath(instance._type)) + "/" + (id instanceof Array ? id.join("/") : id)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(instance._type)
|
||||
const path = typeModelToRestPath(typeModel) + "/" + (id instanceof Array ? id.join("/") : id)
|
||||
const headers = this.userFacade.createAuthHeaders()
|
||||
headers.v = String(instance.typeModel.version)
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { IServiceExecutor } from "../../common/ServiceRequest"
|
|||
import { UpdateSessionKeysService } from "../../entities/sys/Services"
|
||||
import { UserFacade } from "../facades/UserFacade"
|
||||
import { TypeModel } from "../../common/EntityTypes.js"
|
||||
import { resolveClientTypeReference } from "../../common/EntityFunctions.js"
|
||||
import { TypeModelResolver } from "../../common/EntityFunctions"
|
||||
|
||||
assertWorkerOrNode()
|
||||
|
||||
|
@ -27,6 +27,7 @@ export class OwnerEncSessionKeysUpdateQueue {
|
|||
constructor(
|
||||
private readonly userFacade: UserFacade,
|
||||
private readonly serviceExecutor: IServiceExecutor,
|
||||
private readonly typeModelResolver: TypeModelResolver,
|
||||
// allow passing the timeout for testability
|
||||
debounceTimeoutMs: number = UPDATE_SESSION_KEYS_SERVICE_DEBOUNCE_MS,
|
||||
) {
|
||||
|
@ -41,7 +42,7 @@ export class OwnerEncSessionKeysUpdateQueue {
|
|||
*/
|
||||
async updateInstanceSessionKeys(instanceSessionKeys: Array<InstanceSessionKey>, typeModel: TypeModel) {
|
||||
if (this.userFacade.isLeader()) {
|
||||
const groupKeyUpdateTypeModel = await resolveClientTypeReference(GroupKeyUpdateTypeRef)
|
||||
const groupKeyUpdateTypeModel = await this.typeModelResolver.resolveClientTypeReference(GroupKeyUpdateTypeRef)
|
||||
if (groupKeyUpdateTypeModel.id !== typeModel.id) {
|
||||
this.updateInstanceSessionKeyQueue.push(...instanceSessionKeys)
|
||||
this.invokeUpdateSessionKeyService()
|
||||
|
|
|
@ -4,12 +4,12 @@ import { BlobAccessTokenService } from "../../entities/storage/Services"
|
|||
import { IServiceExecutor } from "../../common/ServiceRequest"
|
||||
import { BlobServerAccessInfo, createBlobAccessTokenPostIn, createBlobReadData, createBlobWriteData, createInstanceId } from "../../entities/storage/TypeRefs"
|
||||
import { DateProvider } from "../../common/DateProvider.js"
|
||||
import { resolveClientTypeReference } from "../../common/EntityFunctions.js"
|
||||
import { AuthDataProvider } from "./UserFacade.js"
|
||||
import { deduplicate, first, isEmpty, lazyMemoized, TypeRef } from "@tutao/tutanota-utils"
|
||||
import { ProgrammingError } from "../../common/error/ProgrammingError.js"
|
||||
import { BlobLoadOptions } from "./lazy/BlobFacade.js"
|
||||
import { BlobReferencingInstance } from "../../common/utils/BlobUtils.js"
|
||||
import { TypeModelResolver } from "../../common/EntityFunctions"
|
||||
|
||||
assertWorkerOrNode()
|
||||
|
||||
|
@ -26,7 +26,12 @@ export class BlobAccessTokenFacade {
|
|||
// cache for upload requests are valid for the whole archive (key:<ownerGroup + archiveDataType>).
|
||||
private readonly writeCache: BlobAccessTokenCache
|
||||
|
||||
constructor(private readonly serviceExecutor: IServiceExecutor, private readonly authDataProvider: AuthDataProvider, dateProvider: DateProvider) {
|
||||
constructor(
|
||||
private readonly serviceExecutor: IServiceExecutor,
|
||||
private readonly authDataProvider: AuthDataProvider,
|
||||
dateProvider: DateProvider,
|
||||
private readonly typeModelResolver: TypeModelResolver,
|
||||
) {
|
||||
this.readCache = new BlobAccessTokenCache(dateProvider)
|
||||
this.writeCache = new BlobAccessTokenCache(dateProvider)
|
||||
}
|
||||
|
@ -220,7 +225,7 @@ export class BlobAccessTokenFacade {
|
|||
* @param typeRef the typeRef that shall be used to determine the correct model version
|
||||
*/
|
||||
public async createQueryParams(blobServerAccessInfo: BlobServerAccessInfo, additionalRequestParams: Dict, typeRef: TypeRef<any>): Promise<Dict> {
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
return Object.assign(
|
||||
additionalRequestParams,
|
||||
{
|
||||
|
|
|
@ -52,10 +52,10 @@ import {
|
|||
UserTypeRef,
|
||||
} from "../../entities/sys/TypeRefs.js"
|
||||
import { TutanotaPropertiesTypeRef } from "../../entities/tutanota/TypeRefs.js"
|
||||
import { HttpMethod, MediaType, resolveClientTypeReference } from "../../common/EntityFunctions"
|
||||
import { HttpMethod, MediaType, TypeModelResolver } from "../../common/EntityFunctions"
|
||||
import { assertWorkerOrNode, isAdminClient } from "../../common/Env"
|
||||
import { ConnectMode, EventBusClient } from "../EventBusClient"
|
||||
import { EntityRestClient, typeRefToRestPath } from "../rest/EntityRestClient"
|
||||
import { EntityRestClient, typeModelToRestPath } from "../rest/EntityRestClient"
|
||||
import { AccessExpiredError, ConnectionError, LockedError, NotAuthenticatedError, NotFoundError, SessionExpiredError } from "../../common/error/RestError"
|
||||
import { CancelledError } from "../../common/error/CancelledError"
|
||||
import { RestClient } from "../rest/RestClient"
|
||||
|
@ -220,6 +220,7 @@ export class LoginFacade {
|
|||
private readonly noncachingEntityClient: EntityClient,
|
||||
private readonly sendError: (error: Error) => Promise<void>,
|
||||
private readonly cacheManagementFacade: lazyAsync<CacheManagementFacade>,
|
||||
private readonly typeModelResolver: TypeModelResolver,
|
||||
) {}
|
||||
|
||||
init(eventBusClient: EventBusClient) {
|
||||
|
@ -864,8 +865,9 @@ export class LoginFacade {
|
|||
* @param pushIdentifier identifier associated with this device, if any, to delete PushIdentifier on the server
|
||||
*/
|
||||
async deleteSession(accessToken: Base64Url, pushIdentifier: string | null = null): Promise<void> {
|
||||
let path = (await typeRefToRestPath(SessionTypeRef)) + "/" + this.getSessionListId(accessToken) + "/" + this.getSessionElementId(accessToken)
|
||||
const sessionTypeModel = await resolveClientTypeReference(SessionTypeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(SessionTypeRef)
|
||||
let path = typeModelToRestPath(typeModel) + "/" + this.getSessionListId(accessToken) + "/" + this.getSessionElementId(accessToken)
|
||||
const sessionTypeModel = await this.typeModelResolver.resolveClientTypeReference(SessionTypeRef)
|
||||
|
||||
const headers = {
|
||||
accessToken: neverNull(accessToken),
|
||||
|
@ -904,8 +906,9 @@ export class LoginFacade {
|
|||
userId: Id
|
||||
accessKey: AesKey | null
|
||||
}> {
|
||||
const path = (await typeRefToRestPath(SessionTypeRef)) + "/" + this.getSessionListId(accessToken) + "/" + this.getSessionElementId(accessToken)
|
||||
const SessionTypeModel = await resolveClientTypeReference(SessionTypeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(SessionTypeRef)
|
||||
const path = typeModelToRestPath(typeModel) + "/" + this.getSessionListId(accessToken) + "/" + this.getSessionElementId(accessToken)
|
||||
const SessionTypeModel = await this.typeModelResolver.resolveClientTypeReference(SessionTypeRef)
|
||||
|
||||
let headers = {
|
||||
accessToken: accessToken,
|
||||
|
@ -1042,8 +1045,9 @@ export class LoginFacade {
|
|||
() => this.cryptoFacade,
|
||||
this.instancePipeline,
|
||||
this.blobAccessTokenFacade,
|
||||
this.typeModelResolver,
|
||||
)
|
||||
const entityClient = new EntityClient(eventRestClient)
|
||||
const entityClient = new EntityClient(eventRestClient, this.typeModelResolver)
|
||||
const createSessionReturn = await this.serviceExecutor.post(SessionService, sessionData) // Don't pass email address to avoid proposing to reset second factor when we're resetting password
|
||||
|
||||
const { userId, accessToken } = await this.waitUntilSecondFactorApprovedOrCancelled(createSessionReturn, null)
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from "@tutao/tutanota-utils"
|
||||
import { ArchiveDataType, MAX_BLOB_SIZE_BYTES } from "../../../common/TutanotaConstants.js"
|
||||
|
||||
import { HttpMethod, MediaType, resolveClientTypeReference } from "../../../common/EntityFunctions.js"
|
||||
import { HttpMethod, MediaType } from "../../../common/EntityFunctions.js"
|
||||
import { assertWorkerOrNode, isApp, isDesktop } from "../../../common/Env.js"
|
||||
import type { SuspensionHandler } from "../../SuspensionHandler.js"
|
||||
import { BlobService } from "../../../entities/storage/Services.js"
|
||||
|
@ -352,7 +352,6 @@ export class BlobFacade {
|
|||
|
||||
// Visible for testing
|
||||
public async parseBlobPostOutResponse(jsonData: string): Promise<BlobReferenceTokenWrapper> {
|
||||
const responseTypeModel = await resolveClientTypeReference(BlobPostOutTypeRef)
|
||||
const instance = AttributeModel.removeNetworkDebuggingInfoIfNeeded<ServerModelUntypedInstance>(JSON.parse(jsonData))
|
||||
const { blobReferenceToken } = await this.instancePipeline.decryptAndMap(BlobPostOutTypeRef, instance, null)
|
||||
// is null in case of post multiple to the BlobService, currently only supported in the rust-sdk
|
||||
|
|
|
@ -93,9 +93,6 @@ export type CalendarEventUidIndexEntry = {
|
|||
}
|
||||
|
||||
export class CalendarFacade {
|
||||
// visible for testing
|
||||
readonly cachingEntityClient: EntityClient
|
||||
|
||||
constructor(
|
||||
private readonly userFacade: UserFacade,
|
||||
private readonly groupManagementFacade: GroupManagementFacade,
|
||||
|
@ -108,9 +105,9 @@ export class CalendarFacade {
|
|||
private readonly cryptoFacade: CryptoFacade,
|
||||
private readonly infoMessageHandler: InfoMessageHandler,
|
||||
private readonly instancePipeline: InstancePipeline,
|
||||
) {
|
||||
this.cachingEntityClient = new EntityClient(this.entityRestCache)
|
||||
}
|
||||
// visible for testing
|
||||
public readonly cachingEntityClient: EntityClient,
|
||||
) {}
|
||||
|
||||
async saveImportedCalendarEvents(eventWrappers: Array<EventWrapper>, operationId: OperationId): Promise<void> {
|
||||
// it is safe to assume that all event uids are set at this time
|
||||
|
|
|
@ -19,7 +19,7 @@ import { DbError } from "../../../common/error/DbError.js"
|
|||
import { checkKeyVersionConstraints, KeyLoaderFacade } from "../KeyLoaderFacade.js"
|
||||
import type { QueuedBatch } from "../../EventQueue.js"
|
||||
import { encryptKeyWithVersionedKey, VersionedKey } from "../../crypto/CryptoWrapper.js"
|
||||
import { isUpdateForTypeRef } from "../../../common/utils/EntityUpdateUtils"
|
||||
import { EntityUpdateData, isUpdateForTypeRef } from "../../../common/utils/EntityUpdateUtils"
|
||||
|
||||
const VERSION: number = 2
|
||||
const DB_KEY_PREFIX: string = "ConfigStorage"
|
||||
|
@ -128,8 +128,7 @@ export class ConfigurationDatabase {
|
|||
}
|
||||
}
|
||||
|
||||
async onEntityEventsReceived(batch: QueuedBatch): Promise<any> {
|
||||
const { events, groupId, batchId } = batch
|
||||
async onEntityEventsReceived(events: readonly EntityUpdateData[], _batchId: Id, _groupId: Id): Promise<any> {
|
||||
for (const event of events) {
|
||||
if (!(event.operation === OperationType.UPDATE && isUpdateForTypeRef(UserTypeRef, event))) {
|
||||
continue
|
||||
|
|
|
@ -153,7 +153,7 @@ import { KeyLoaderFacade, parseKeyVersion } from "../KeyLoaderFacade.js"
|
|||
import { encryptBytes, encryptKeyWithVersionedKey, encryptString, VersionedEncryptedKey, VersionedKey } from "../../crypto/CryptoWrapper.js"
|
||||
import { PublicKeyProvider } from "../PublicKeyProvider.js"
|
||||
import { KeyVerificationMismatchError } from "../../../common/error/KeyVerificationMismatchError"
|
||||
import { isUpdateForTypeRef } from "../../../common/utils/EntityUpdateUtils"
|
||||
import { EntityUpdateData, isUpdateForTypeRef } from "../../../common/utils/EntityUpdateUtils"
|
||||
|
||||
assertWorkerOrNode()
|
||||
type Attachments = ReadonlyArray<TutanotaFile | DataFile | FileReference>
|
||||
|
@ -892,7 +892,7 @@ export class MailFacade {
|
|||
.catch(ofClass(NotFoundError, () => null))
|
||||
}
|
||||
|
||||
entityEventsReceived(data: EntityUpdate[]): Promise<void> {
|
||||
entityEventsReceived(data: readonly EntityUpdateData[]): Promise<void> {
|
||||
return promiseMap(data, (update) => {
|
||||
if (
|
||||
this.deferredDraftUpdate != null &&
|
||||
|
|
|
@ -24,7 +24,6 @@ import {
|
|||
TypeRef,
|
||||
} from "@tutao/tutanota-utils"
|
||||
import { isDesktop, isOfflineStorageAvailable, isTest } from "../../common/Env.js"
|
||||
import { resolveClientTypeReference, resolveServerTypeReference } from "../../common/EntityFunctions.js"
|
||||
import { DateProvider } from "../../common/DateProvider.js"
|
||||
import { TokenOrNestedTokens } from "cborg/interface"
|
||||
import { CalendarEventTypeRef, MailTypeRef } from "../../entities/tutanota/TypeRefs.js"
|
||||
|
@ -39,6 +38,7 @@ import { OutOfSyncError } from "../../common/error/OutOfSyncError.js"
|
|||
import { sql, SqlFragment } from "./Sql.js"
|
||||
import { ModelMapper } from "../crypto/ModelMapper"
|
||||
import { AttributeModel } from "../../common/AttributeModel"
|
||||
import { TypeModelResolver } from "../../common/EntityFunctions"
|
||||
|
||||
/**
|
||||
* this is the value of SQLITE_MAX_VARIABLE_NUMBER in sqlite3.c
|
||||
|
@ -115,6 +115,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
private readonly migrator: OfflineStorageMigrator,
|
||||
private readonly cleaner: OfflineStorageCleaner,
|
||||
private readonly modelMapper: ModelMapper,
|
||||
private readonly typeModelResolver: TypeModelResolver,
|
||||
) {
|
||||
assert(isOfflineStorageAvailable() || isTest(), "Offline storage is not available.")
|
||||
}
|
||||
|
@ -195,7 +196,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
|
||||
async deleteIfExists(typeRef: TypeRef<SomeEntity>, listId: Id | null, elementId: Id): Promise<void> {
|
||||
const type = getTypeString(typeRef)
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
const encodedElementId = ensureBase64Ext(typeModel, elementId)
|
||||
let formattedQuery
|
||||
switch (typeModel.type) {
|
||||
|
@ -227,7 +228,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
|
||||
async deleteAllOfType(typeRef: TypeRef<SomeEntity>): Promise<void> {
|
||||
const type = getTypeString(typeRef)
|
||||
let typeModel = await resolveClientTypeReference(typeRef)
|
||||
let typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
let formattedQuery
|
||||
switch (typeModel.type) {
|
||||
case TypeId.Element:
|
||||
|
@ -270,7 +271,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
|
||||
async getParsed(typeRef: TypeRef<unknown>, listId: Id | null, id: Id): Promise<ServerModelParsedInstance | null> {
|
||||
const type = getTypeString(typeRef)
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
const encodedElementId = ensureBase64Ext(typeModel, id)
|
||||
let formattedQuery
|
||||
switch (typeModel.type) {
|
||||
|
@ -303,7 +304,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
|
||||
async provideMultipleParsed<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, elementIds: Id[]): Promise<Array<ServerModelParsedInstance>> {
|
||||
if (elementIds.length === 0) return []
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
const encodedElementIds = elementIds.map((elementId) => ensureBase64Ext(typeModel, elementId))
|
||||
|
||||
const type = getTypeString(typeRef)
|
||||
|
@ -321,7 +322,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
|
||||
async getIdsInRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id): Promise<Array<Id>> {
|
||||
const type = getTypeString(typeRef)
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
const range = await this.getRange(typeRef, listId)
|
||||
if (range == null) {
|
||||
throw new Error(`no range exists for ${type} and list ${listId}`)
|
||||
|
@ -343,7 +344,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
async getRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id): Promise<Range | null> {
|
||||
let range = await this.getRange(typeRef, listId)
|
||||
if (range == null) return range
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
return {
|
||||
lower: customIdToBase64Url(typeModel, range.lower),
|
||||
upper: customIdToBase64Url(typeModel, range.upper),
|
||||
|
@ -351,7 +352,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
}
|
||||
|
||||
async isElementIdInCacheRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, elementId: Id): Promise<boolean> {
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
const encodedElementId = ensureBase64Ext(typeModel, elementId)
|
||||
|
||||
const range = await this.getRange(typeRef, listId)
|
||||
|
@ -365,7 +366,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
count: number,
|
||||
reverse: boolean,
|
||||
): Promise<ServerModelParsedInstance[]> {
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
const encodedStartId = ensureBase64Ext(typeModel, start)
|
||||
const type = getTypeString(typeRef)
|
||||
let formattedQuery
|
||||
|
@ -396,7 +397,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
|
||||
async put(typeRef: TypeRef<unknown>, instance: ServerModelParsedInstance): Promise<void> {
|
||||
const serializedInstance = await this.serialize(instance)
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
|
||||
const { listId, elementId } = expandId(AttributeModel.getAttribute<IdTuple | Id>(instance, "_id", typeModel))
|
||||
const ownerGroup = AttributeModel.getAttribute<Id>(instance, "_ownerGroup", typeModel)
|
||||
|
@ -440,7 +441,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
}
|
||||
|
||||
async setLowerRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, lowerId: Id): Promise<void> {
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
let typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
lowerId = ensureBase64Ext(typeModel, lowerId)
|
||||
const type = getTypeString(typeRef)
|
||||
|
||||
|
@ -457,7 +458,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
}
|
||||
|
||||
async setUpperRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, upperId: Id): Promise<void> {
|
||||
upperId = ensureBase64Ext(await resolveClientTypeReference(typeRef), upperId)
|
||||
upperId = ensureBase64Ext(await this.typeModelResolver.resolveClientTypeReference(typeRef), upperId)
|
||||
const type = getTypeString(typeRef)
|
||||
const { query, params } = sql`UPDATE ranges
|
||||
SET upper = ${upperId}
|
||||
|
@ -467,7 +468,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
}
|
||||
|
||||
async setNewRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, lower: Id, upper: Id): Promise<void> {
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
lower = ensureBase64Ext(typeModel, lower)
|
||||
upper = ensureBase64Ext(typeModel, upper)
|
||||
|
||||
|
@ -555,7 +556,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
this.customCacheHandler = new CustomCacheHandlerMap(
|
||||
{
|
||||
ref: CalendarEventTypeRef,
|
||||
handler: new CustomCalendarEventCacheHandler(entityRestClient),
|
||||
handler: new CustomCalendarEventCacheHandler(entityRestClient, this.typeModelResolver),
|
||||
},
|
||||
{ ref: MailTypeRef, handler: new CustomMailEventCacheHandler() },
|
||||
)
|
||||
|
@ -690,7 +691,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
|
||||
async deleteIn(typeRef: TypeRef<unknown>, listId: Id | null, elementIds: Id[]): Promise<void> {
|
||||
if (elementIds.length === 0) return
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
const encodedElementIds = elementIds.map((elementIds) => ensureBase64Ext(typeModel, elementIds))
|
||||
switch (typeModel.type) {
|
||||
case TypeId.Element:
|
||||
|
@ -728,7 +729,7 @@ export class OfflineStorage implements CacheStorage {
|
|||
}
|
||||
|
||||
async updateRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, rawCutoffId: Id): Promise<void> {
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
const isCustomId = isCustomIdType(typeModel)
|
||||
const encodedCutoffId = ensureBase64Ext(typeModel, rawCutoffId)
|
||||
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { QueuedBatch } from "../EventQueue.js"
|
||||
import { EntityUpdate } from "../../entities/sys/TypeRefs.js"
|
||||
import { ListElementEntity, SomeEntity } from "../../common/EntityTypes"
|
||||
import { ProgrammingError } from "../../common/error/ProgrammingError"
|
||||
import { TypeRef } from "@tutao/tutanota-utils"
|
||||
import { EntityRestCache } from "./DefaultEntityRestCache.js"
|
||||
import { EntityRestClientLoadOptions } from "./EntityRestClient.js"
|
||||
import { EntityUpdateData } from "../../common/utils/EntityUpdateUtils"
|
||||
|
||||
export class AdminClientDummyEntityRestCache implements EntityRestCache {
|
||||
async entityEventsReceived(batch: QueuedBatch): Promise<Array<EntityUpdate>> {
|
||||
return batch.events
|
||||
async entityEventsReceived(events: readonly EntityUpdateData[], batchId: Id, groupId: Id): Promise<readonly EntityUpdateData[]> {
|
||||
return events
|
||||
}
|
||||
|
||||
async erase<T extends SomeEntity>(instance: T): Promise<void> {
|
||||
|
|
|
@ -47,8 +47,8 @@ export class LateInitializedCacheStorageImpl implements CacheStorageLateInitiali
|
|||
private _inner: SomeStorage | null = null
|
||||
|
||||
constructor(
|
||||
private readonly modelMapper: ModelMapper,
|
||||
private readonly sendError: (error: Error) => Promise<void>,
|
||||
private readonly ephemeralStorageProvider: () => Promise<EphemeralCacheStorage>,
|
||||
private readonly offlineStorageProvider: () => Promise<null | OfflineStorage>,
|
||||
) {}
|
||||
|
||||
|
@ -118,7 +118,7 @@ export class LateInitializedCacheStorageImpl implements CacheStorageLateInitiali
|
|||
}
|
||||
}
|
||||
// both "else" case and fallback for unavailable storage and error cases
|
||||
const storage = new EphemeralCacheStorage(this.modelMapper)
|
||||
const storage = await this.ephemeralStorageProvider()
|
||||
storage.init(args)
|
||||
return {
|
||||
storage,
|
||||
|
|
|
@ -2,12 +2,13 @@ import { ListElementEntity, ServerModelParsedInstance, TypeModel } from "../../c
|
|||
import { CalendarEvent, CalendarEventTypeRef, Mail } from "../../entities/tutanota/TypeRefs.js"
|
||||
import { freezeMap, getTypeString, TypeRef } from "@tutao/tutanota-utils"
|
||||
import { CUSTOM_MAX_ID, CUSTOM_MIN_ID, elementIdPart, firstBiggerThanSecond, getElementId, LOAD_MULTIPLE_LIMIT } from "../../common/utils/EntityUtils.js"
|
||||
import { resolveServerTypeReference } from "../../common/EntityFunctions.js"
|
||||
import { CacheStorage, ExposedCacheStorage, Range } from "./DefaultEntityRestCache.js"
|
||||
import { EntityRestClient } from "./EntityRestClient.js"
|
||||
import { ProgrammingError } from "../../common/error/ProgrammingError.js"
|
||||
import { EntityUpdate } from "../../entities/sys/TypeRefs"
|
||||
import { AttributeModel } from "../../common/AttributeModel"
|
||||
import { TypeModelResolver } from "../../common/EntityFunctions"
|
||||
import { EntityUpdateData } from "../../common/utils/EntityUpdateUtils"
|
||||
|
||||
/**
|
||||
* update when implementing custom cache handlers.
|
||||
|
@ -61,7 +62,7 @@ export interface CustomCacheHandler<T extends ListElementEntity> {
|
|||
|
||||
getElementIdsInCacheRange?: (storage: ExposedCacheStorage, listId: Id, ids: Array<Id>) => Promise<Array<Id>>
|
||||
|
||||
shouldLoadOnCreateEvent?: (event: EntityUpdate) => Promise<boolean>
|
||||
shouldLoadOnCreateEvent?: (event: EntityUpdateData) => Promise<boolean>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -69,11 +70,11 @@ export interface CustomCacheHandler<T extends ListElementEntity> {
|
|||
* this effectively in the database.
|
||||
*/
|
||||
export class CustomCalendarEventCacheHandler implements CustomCacheHandler<CalendarEvent> {
|
||||
constructor(private readonly entityRestClient: EntityRestClient) {}
|
||||
constructor(private readonly entityRestClient: EntityRestClient, private readonly typeModelResolver: TypeModelResolver) {}
|
||||
|
||||
async loadRange(storage: CacheStorage, listId: Id, start: Id, count: number, reverse: boolean): Promise<CalendarEvent[]> {
|
||||
const range = await storage.getRangeForList(CalendarEventTypeRef, listId)
|
||||
const typeModel = await resolveServerTypeReference(CalendarEventTypeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(CalendarEventTypeRef)
|
||||
|
||||
// if offline db for this list is empty load from server
|
||||
let rawList: Array<ServerModelParsedInstance> = []
|
||||
|
|
|
@ -8,14 +8,12 @@ import {
|
|||
getCacheModeBehavior,
|
||||
OwnerEncSessionKeyProvider,
|
||||
} from "./EntityRestClient"
|
||||
import { resolveClientTypeReference, resolveServerTypeReference, resolveTypeRefFromAppAndTypeNameLegacy } from "../../common/EntityFunctions"
|
||||
import { OperationType } from "../../common/TutanotaConstants"
|
||||
import { assertNotNull, difference, getFirstOrThrow, getTypeString, groupBy, isSameTypeRef, lastThrow, TypeRef } from "@tutao/tutanota-utils"
|
||||
import {
|
||||
AuditLogEntryTypeRef,
|
||||
BucketPermissionTypeRef,
|
||||
EntityEventBatchTypeRef,
|
||||
EntityUpdate,
|
||||
GroupKeyTypeRef,
|
||||
GroupTypeRef,
|
||||
KeyRotationTypeRef,
|
||||
|
@ -46,13 +44,12 @@ import {
|
|||
import { ProgrammingError } from "../../common/error/ProgrammingError"
|
||||
import { assertWorkerOrNode } from "../../common/Env"
|
||||
import type { Entity, ListElementEntity, ServerModelParsedInstance, SomeEntity, TypeModel } from "../../common/EntityTypes"
|
||||
import { QueuedBatch } from "../EventQueue.js"
|
||||
import { ENTITY_EVENT_BATCH_EXPIRE_MS } from "../EventBusClient"
|
||||
import { CustomCacheHandlerMap } from "./CustomCacheHandler.js"
|
||||
import { containsEventOfType, entityUpdateToUpdateData, getEventOfType, isUpdateForTypeRef } from "../../common/utils/EntityUpdateUtils.js"
|
||||
import { containsEventOfType, EntityUpdateData, getEventOfType, isUpdateForTypeRef } from "../../common/utils/EntityUpdateUtils.js"
|
||||
import { isCustomIdType } from "../offline/OfflineStorage.js"
|
||||
import { AppName } from "@tutao/tutanota-utils/dist/TypeRef"
|
||||
import { AttributeModel } from "../../common/AttributeModel"
|
||||
import { TypeModelResolver } from "../../common/EntityFunctions"
|
||||
|
||||
assertWorkerOrNode()
|
||||
|
||||
|
@ -273,7 +270,11 @@ export interface CacheStorage extends ExposedCacheStorage {
|
|||
* lowerRangeId may be anything from MIN_ID to c, upperRangeId may be anything from k to MAX_ID
|
||||
*/
|
||||
export class DefaultEntityRestCache implements EntityRestCache {
|
||||
constructor(private readonly entityRestClient: EntityRestClient, private readonly storage: CacheStorage) {}
|
||||
constructor(
|
||||
private readonly entityRestClient: EntityRestClient,
|
||||
private readonly storage: CacheStorage,
|
||||
private readonly typeModelResolver: TypeModelResolver,
|
||||
) {}
|
||||
|
||||
async load<T extends SomeEntity>(typeRef: TypeRef<T>, id: PropertyType<T, "_id">, opts: EntityRestClientLoadOptions = {}): Promise<T> {
|
||||
const useCache = this.shouldUseCache(typeRef, opts)
|
||||
|
@ -431,7 +432,7 @@ export class DefaultEntityRestCache implements EntityRestCache {
|
|||
return await customHandler.loadRange(this.storage, listId, start, count, reverse)
|
||||
}
|
||||
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
const useCache = this.shouldUseCache(typeRef, opts) && isCachedRangeType(typeModel, typeRef)
|
||||
|
||||
if (!useCache) {
|
||||
|
@ -617,8 +618,8 @@ export class DefaultEntityRestCache implements EntityRestCache {
|
|||
wasReverseRequest: boolean,
|
||||
receivedEntities: ServerModelParsedInstance[],
|
||||
) {
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const isCustomId = isCustomIdType(await resolveClientTypeReference(typeRef))
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
const isCustomId = isCustomIdType(await this.typeModelResolver.resolveClientTypeReference(typeRef))
|
||||
let elementsToAdd = receivedEntities
|
||||
if (wasReverseRequest) {
|
||||
// Ensure that elements are cached in ascending (not reverse) order
|
||||
|
@ -674,7 +675,7 @@ export class DefaultEntityRestCache implements EntityRestCache {
|
|||
const { lower, upper } = range
|
||||
let indexOfStart = allRangeList.indexOf(start)
|
||||
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
const isCustomId = isCustomIdType(typeModel)
|
||||
if (
|
||||
(!reverse && (isCustomId ? upper === CUSTOM_MAX_ID : upper === GENERATED_MAX_ID)) ||
|
||||
|
@ -724,16 +725,16 @@ export class DefaultEntityRestCache implements EntityRestCache {
|
|||
*
|
||||
* @return Promise, which resolves to the array of valid events (if response is NotFound or NotAuthorized we filter it out)
|
||||
*/
|
||||
async entityEventsReceived(batch: QueuedBatch): Promise<Array<EntityUpdate>> {
|
||||
async entityEventsReceived(events: readonly EntityUpdateData[], batchId: Id, groupId: Id): Promise<readonly EntityUpdateData[]> {
|
||||
await this.recordSyncTime()
|
||||
|
||||
// we handle post multiple create operations separately to optimize the number of requests with getMultiple
|
||||
const createUpdatesForLETs: EntityUpdate[] = []
|
||||
const regularUpdates: EntityUpdate[] = [] // all updates not resulting from post multiple requests
|
||||
const updatesArray = batch.events
|
||||
for (const update of updatesArray) {
|
||||
const createUpdatesForLETs: EntityUpdateData[] = []
|
||||
const regularUpdates: EntityUpdateData[] = [] // all updates not resulting from post multiple requests
|
||||
|
||||
for (const update of events) {
|
||||
// monitor application is ignored
|
||||
if (update.application === "monitor") continue
|
||||
if (update.typeRef.app === "monitor") continue
|
||||
// mailSetEntries are ignored because move operations are handled as a special event (and no post multiple is possible)
|
||||
if (
|
||||
update.operation === OperationType.CREATE &&
|
||||
|
@ -749,13 +750,11 @@ export class DefaultEntityRestCache implements EntityRestCache {
|
|||
|
||||
const createUpdatesForLETsPerList = groupBy(createUpdatesForLETs, (update) => update.instanceListId)
|
||||
|
||||
const postMultipleEventUpdates: EntityUpdate[][] = []
|
||||
const postMultipleEventUpdates: EntityUpdateData[][] = []
|
||||
// we first handle potential post multiple updates in get multiple requests
|
||||
for (let [instanceListId, updates] of createUpdatesForLETsPerList) {
|
||||
const firstUpdate = updates[0]
|
||||
const typeRef = firstUpdate.typeId
|
||||
? new TypeRef<SomeEntity>(firstUpdate.application as AppName, parseInt(firstUpdate.typeId))
|
||||
: resolveTypeRefFromAppAndTypeNameLegacy(firstUpdate.application as AppName, firstUpdate.type)
|
||||
const typeRef = firstUpdate.typeRef
|
||||
const ids = updates.map((update) => update.instanceId)
|
||||
|
||||
// We only want to load the instances that are in cache range
|
||||
|
@ -792,14 +791,11 @@ export class DefaultEntityRestCache implements EntityRestCache {
|
|||
}
|
||||
}
|
||||
|
||||
const otherEventUpdates: EntityUpdate[] = []
|
||||
const otherEventUpdates: EntityUpdateData[] = []
|
||||
// we need an array of UpdateEntityData
|
||||
for (let update of regularUpdates) {
|
||||
const { operation, typeId, application, type } = update
|
||||
const { operation, typeRef } = update
|
||||
const { instanceListId, instanceId } = getUpdateInstanceId(update)
|
||||
const typeRef = typeId
|
||||
? new TypeRef<SomeEntity>(application as AppName, parseInt(typeId))
|
||||
: resolveTypeRefFromAppAndTypeNameLegacy(application as AppName, type)
|
||||
|
||||
switch (operation) {
|
||||
case OperationType.UPDATE: {
|
||||
|
@ -810,17 +806,9 @@ export class DefaultEntityRestCache implements EntityRestCache {
|
|||
break // do break instead of continue to avoid ide warnings
|
||||
}
|
||||
case OperationType.DELETE: {
|
||||
if (
|
||||
isSameTypeRef(MailSetEntryTypeRef, typeRef) &&
|
||||
containsEventOfType(
|
||||
updatesArray.map((e) => entityUpdateToUpdateData(e)),
|
||||
OperationType.CREATE,
|
||||
instanceId,
|
||||
)
|
||||
) {
|
||||
if (isSameTypeRef(MailSetEntryTypeRef, typeRef) && containsEventOfType(events, OperationType.CREATE, instanceId)) {
|
||||
// move for mail is handled in create event.
|
||||
} else if (isSameTypeRef(MailTypeRef, typeRef)) {
|
||||
const mailTypeModel = await resolveServerTypeReference(MailTypeRef)
|
||||
// delete mailDetails if they are available (as we don't send an event for this type)
|
||||
const mail = await this.storage.get(typeRef, instanceListId, instanceId)
|
||||
if (mail) {
|
||||
|
@ -837,7 +825,7 @@ export class DefaultEntityRestCache implements EntityRestCache {
|
|||
break // do break instead of continue to avoid ide warnings
|
||||
}
|
||||
case OperationType.CREATE: {
|
||||
const handledUpdate = await this.processCreateEvent(typeRef, update, updatesArray)
|
||||
const handledUpdate = await this.processCreateEvent(typeRef, update, events)
|
||||
if (handledUpdate) {
|
||||
otherEventUpdates.push(handledUpdate)
|
||||
}
|
||||
|
@ -848,13 +836,17 @@ export class DefaultEntityRestCache implements EntityRestCache {
|
|||
}
|
||||
}
|
||||
// the whole batch has been written successfully
|
||||
await this.storage.putLastBatchIdForGroup(batch.groupId, batch.batchId)
|
||||
await this.storage.putLastBatchIdForGroup(groupId, batchId)
|
||||
// merge the results
|
||||
return otherEventUpdates.concat(postMultipleEventUpdates.flat())
|
||||
}
|
||||
|
||||
/** Returns {null} when the update should be skipped. */
|
||||
private async processCreateEvent(typeRef: TypeRef<any>, update: EntityUpdate, batch: ReadonlyArray<EntityUpdate>): Promise<EntityUpdate | null> {
|
||||
private async processCreateEvent(
|
||||
typeRef: TypeRef<any>,
|
||||
update: EntityUpdateData,
|
||||
batch: ReadonlyArray<EntityUpdateData>,
|
||||
): Promise<EntityUpdateData | null> {
|
||||
// do not return undefined to avoid implicit returns
|
||||
const { instanceId, instanceListId } = getUpdateInstanceId(update)
|
||||
|
||||
|
@ -907,7 +899,7 @@ export class DefaultEntityRestCache implements EntityRestCache {
|
|||
*/
|
||||
private async updateListIdOfMailSetEntryAndUpdateCache(mailSetEntry: ServerModelParsedInstance, newListId: Id, elementId: Id) {
|
||||
// In case of a move operation we have to replace the list id always, as the mailSetEntry is stored in another folder.
|
||||
const typeModel = await resolveServerTypeReference(MailSetEntryTypeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(MailSetEntryTypeRef)
|
||||
const attributeId = AttributeModel.getAttributeId(typeModel, "_id")
|
||||
if (attributeId == null) {
|
||||
throw new ProgrammingError("no _id for mail set entry in type model ")
|
||||
|
@ -917,7 +909,7 @@ export class DefaultEntityRestCache implements EntityRestCache {
|
|||
}
|
||||
|
||||
/** Returns {null} when the update should be skipped. */
|
||||
private async processUpdateEvent(typeRef: TypeRef<SomeEntity>, update: EntityUpdate): Promise<EntityUpdate | null> {
|
||||
private async processUpdateEvent(typeRef: TypeRef<SomeEntity>, update: EntityUpdateData): Promise<EntityUpdateData | null> {
|
||||
const { instanceId, instanceListId } = getUpdateInstanceId(update)
|
||||
const cached = await this.storage.getParsed(typeRef, instanceListId, instanceId)
|
||||
// No need to try to download something that's not there anymore
|
||||
|
@ -1034,7 +1026,7 @@ export function collapseId(listId: Id | null, elementId: Id): Id | IdTuple {
|
|||
}
|
||||
}
|
||||
|
||||
export function getUpdateInstanceId(update: EntityUpdate): { instanceListId: Id | null; instanceId: Id } {
|
||||
export function getUpdateInstanceId(update: EntityUpdateData): { instanceListId: Id | null; instanceId: Id } {
|
||||
let instanceListId
|
||||
if (update.instanceListId === "") {
|
||||
instanceListId = null
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { RestClient, SuspensionBehavior } from "./RestClient"
|
||||
import { CryptoFacade } from "../crypto/CryptoFacade"
|
||||
import { _verifyType, HttpMethod, MediaType, resolveClientTypeReference, resolveServerTypeReference } from "../../common/EntityFunctions"
|
||||
import { _verifyType, HttpMethod, MediaType, TypeModelResolver } from "../../common/EntityFunctions"
|
||||
import { SessionKeyNotFoundError } from "../../common/error/SessionKeyNotFoundError"
|
||||
import type { EntityUpdate } from "../../entities/sys/TypeRefs.js"
|
||||
import {
|
||||
|
@ -44,12 +44,12 @@ import { EntityAdapter } from "../crypto/EntityAdapter"
|
|||
import { parseKeyVersion } from "../facades/KeyLoaderFacade"
|
||||
import { AttributeModel } from "../../common/AttributeModel"
|
||||
import { PersistenceResourcePostReturnTypeRef } from "../../entities/base/TypeRefs"
|
||||
import { EntityUpdateData } from "../../common/utils/EntityUpdateUtils"
|
||||
|
||||
assertWorkerOrNode()
|
||||
|
||||
export async function typeRefToRestPath(typeRef: TypeRef<any>): Promise<string> {
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
return `/rest/${typeRef.app}/${typeModel.name.toLowerCase()}`
|
||||
export function typeModelToRestPath(typeModel: TypeModel): string {
|
||||
return `/rest/${typeModel.app}/${typeModel.name.toLowerCase()}`
|
||||
}
|
||||
|
||||
export interface EntityRestClientSetupOptions {
|
||||
|
@ -195,10 +195,9 @@ export interface EntityRestInterface {
|
|||
|
||||
/**
|
||||
* Must be called when entity events are received.
|
||||
* @param batch The entity events that were received.
|
||||
* @return Similar to the events in the data parameter, but reduced by the events which are obsolete.
|
||||
*/
|
||||
entityEventsReceived(batch: QueuedBatch): Promise<Array<EntityUpdate>>
|
||||
entityEventsReceived(events: readonly EntityUpdateData[], batchId: Id, groupId: Id): Promise<readonly EntityUpdateData[]>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -221,6 +220,7 @@ export class EntityRestClient implements EntityRestInterface {
|
|||
private readonly lazyCrypto: lazy<CryptoFacade>,
|
||||
private readonly instancePipeline: InstancePipeline,
|
||||
private readonly blobAccessTokenFacade: BlobAccessTokenFacade,
|
||||
private readonly typeModelResolver: TypeModelResolver,
|
||||
) {}
|
||||
|
||||
async loadParsedInstance<T extends SomeEntity>(
|
||||
|
@ -243,7 +243,7 @@ export class EntityRestClient implements EntityRestInterface {
|
|||
responseType: MediaType.Json,
|
||||
baseUrl: opts.baseUrl,
|
||||
})
|
||||
const serverTypeModel = await resolveServerTypeReference(typeRef)
|
||||
const serverTypeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
const untypedInstance = AttributeModel.removeNetworkDebuggingInfoIfNeeded<ServerModelUntypedInstance>(JSON.parse(json))
|
||||
|
||||
const encryptedParsedInstance = await this.instancePipeline.typeMapper.applyJsTypes(serverTypeModel, untypedInstance)
|
||||
|
@ -352,7 +352,7 @@ export class EntityRestClient implements EntityRestInterface {
|
|||
): Promise<Array<ServerModelParsedInstance>> {
|
||||
const { path, headers } = await this._validateAndPrepareRestRequest(typeRef, listId, null, opts.queryParams, opts.extraHeaders, opts.ownerKeyProvider)
|
||||
const idChunks = splitInChunks(LOAD_MULTIPLE_LIMIT, elementIds)
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
|
||||
const loadedChunks = await promiseMap(idChunks, async (idChunk) => {
|
||||
let queryParams = {
|
||||
|
@ -440,7 +440,7 @@ export class EntityRestClient implements EntityRestInterface {
|
|||
loadedEntities: Array<ServerModelUntypedInstance>,
|
||||
ownerEncSessionKeyProvider?: OwnerEncSessionKeyProvider,
|
||||
): Promise<Array<ServerModelParsedInstance>> {
|
||||
const serverTypeModel = await resolveServerTypeReference(typeRef)
|
||||
const serverTypeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
return await promiseMap(
|
||||
loadedEntities,
|
||||
async (instance) => {
|
||||
|
@ -513,7 +513,7 @@ export class EntityRestClient implements EntityRestInterface {
|
|||
body: JSON.stringify(untypedInstance),
|
||||
responseType: MediaType.Json,
|
||||
})
|
||||
const postReturnTypeModel = await resolveClientTypeReference(PersistenceResourcePostReturnTypeRef)
|
||||
const postReturnTypeModel = await this.typeModelResolver.resolveClientTypeReference(PersistenceResourcePostReturnTypeRef)
|
||||
const untypedPersistencePostReturn = AttributeModel.removeNetworkDebuggingInfoIfNeeded<ClientModelUntypedInstance>(JSON.parse(persistencePostReturn))
|
||||
return AttributeModel.getAttributeorNull<Id>(untypedPersistencePostReturn, "generatedId", postReturnTypeModel)
|
||||
}
|
||||
|
@ -658,7 +658,7 @@ export class EntityRestClient implements EntityRestInterface {
|
|||
headers: Dict | undefined
|
||||
clientTypeModel: ClientTypeModel
|
||||
}> {
|
||||
const clientTypeModel = await resolveClientTypeReference(typeRef)
|
||||
const clientTypeModel = await this.typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
|
||||
_verifyType(clientTypeModel)
|
||||
|
||||
|
@ -667,7 +667,7 @@ export class EntityRestClient implements EntityRestInterface {
|
|||
throw new LoginIncompleteError(`Trying to do a network request with encrypted entity but is not fully logged in yet, type: ${clientTypeModel.name}`)
|
||||
}
|
||||
|
||||
let path = await typeRefToRestPath(typeRef)
|
||||
let path = typeModelToRestPath(clientTypeModel)
|
||||
|
||||
if (listId) {
|
||||
path += "/" + listId
|
||||
|
@ -695,8 +695,8 @@ export class EntityRestClient implements EntityRestInterface {
|
|||
/**
|
||||
* for the admin area (no cache available)
|
||||
*/
|
||||
entityEventsReceived(batch: QueuedBatch): Promise<Array<EntityUpdate>> {
|
||||
return Promise.resolve(batch.events)
|
||||
entityEventsReceived(events: readonly EntityUpdateData[], batchId: Id, groupId: Id): Promise<readonly EntityUpdateData[]> {
|
||||
return Promise.resolve(events)
|
||||
}
|
||||
|
||||
getRestClient(): RestClient {
|
||||
|
@ -704,7 +704,6 @@ export class EntityRestClient implements EntityRestInterface {
|
|||
}
|
||||
|
||||
private async parseSetupMultiple(result: Array<UntypedInstance>): Promise<Array<Id>> {
|
||||
const persistencePostReturnModel = await resolveServerTypeReference(PersistenceResourcePostReturnTypeRef)
|
||||
try {
|
||||
return await promiseMap(Array.from(result), async (untypedPostReturn: any) => {
|
||||
const sanitisedUntypedPostReturn = AttributeModel.removeNetworkDebuggingInfoIfNeeded<ServerModelUntypedInstance>(untypedPostReturn)
|
||||
|
|
|
@ -4,13 +4,13 @@ import { firstBiggerThanSecond } from "../../common/utils/EntityUtils.js"
|
|||
import { CacheStorage, expandId, LastUpdateTime } from "./DefaultEntityRestCache.js"
|
||||
import { assertNotNull, clone, getFromMap, getTypeString, remove, TypeRef } from "@tutao/tutanota-utils"
|
||||
import { CustomCacheHandlerMap } from "./CustomCacheHandler.js"
|
||||
import { resolveServerTypeReference } from "../../common/EntityFunctions.js"
|
||||
import { Type as TypeId } from "../../common/EntityConstants.js"
|
||||
import { ProgrammingError } from "../../common/error/ProgrammingError.js"
|
||||
import { customIdToBase64Url, ensureBase64Ext } from "../offline/OfflineStorage.js"
|
||||
import { AttributeModel } from "../../common/AttributeModel"
|
||||
import { ModelMapper } from "../crypto/ModelMapper"
|
||||
import { parseTypeString } from "@tutao/tutanota-utils/dist/TypeRef"
|
||||
import { ServerTypeModelResolver } from "../../common/EntityFunctions"
|
||||
|
||||
/** Cache for a single list. */
|
||||
type ListCache = {
|
||||
|
@ -47,7 +47,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
private userId: Id | null = null
|
||||
private lastBatchIdPerGroup = new Map<Id, Id>()
|
||||
|
||||
constructor(private readonly modelMapper: ModelMapper) {}
|
||||
constructor(private readonly modelMapper: ModelMapper, private readonly typeModelResolver: ServerTypeModelResolver) {}
|
||||
|
||||
init({ userId }: EphemeralStorageInitArgs) {
|
||||
this.userId = userId
|
||||
|
@ -68,7 +68,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
async getParsed(typeRef: TypeRef<unknown>, listId: Id | null, id: Id): Promise<ServerModelParsedInstance | null> {
|
||||
// We downcast because we can't prove that map has correct entity on the type level
|
||||
const type = getTypeString(typeRef)
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
id = ensureBase64Ext(typeModel, id)
|
||||
switch (typeModel.type) {
|
||||
case TypeId.Element:
|
||||
|
@ -89,7 +89,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
count: number,
|
||||
reverse: boolean,
|
||||
): Promise<ServerModelParsedInstance[]> {
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
startElementId = ensureBase64Ext(typeModel, startElementId)
|
||||
|
||||
const listCache = this.lists.get(getTypeString(typeRef))?.get(listId)
|
||||
|
@ -136,7 +136,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
async provideMultipleParsed(typeRef: TypeRef<unknown>, listId: string, elementIds: string[]): Promise<ServerModelParsedInstance[]> {
|
||||
const listCache = this.lists.get(getTypeString(typeRef))?.get(listId)
|
||||
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
elementIds = elementIds.map((el) => ensureBase64Ext(typeModel, el))
|
||||
|
||||
if (listCache == null) {
|
||||
|
@ -173,7 +173,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
|
||||
async deleteIfExists<T>(typeRef: TypeRef<T>, listId: Id | null, elementId: Id): Promise<void> {
|
||||
const type = getTypeString(typeRef)
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
elementId = ensureBase64Ext(typeModel, elementId)
|
||||
switch (typeModel.type) {
|
||||
case TypeId.Element:
|
||||
|
@ -200,7 +200,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
}
|
||||
|
||||
async isElementIdInCacheRange(typeRef: TypeRef<unknown>, listId: Id, elementId: Id): Promise<boolean> {
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
elementId = ensureBase64Ext(typeModel, elementId)
|
||||
|
||||
const cache = this.lists.get(getTypeString(typeRef))?.get(listId)
|
||||
|
@ -209,7 +209,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
|
||||
async put(typeRef: TypeRef<unknown>, instance: ServerModelParsedInstance): Promise<void> {
|
||||
const instanceClone = clone(instance)
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
const instanceId = AttributeModel.getAttribute<IdTuple | Id>(instanceClone, "_id", typeModel)
|
||||
let { listId, elementId } = expandId(instanceId)
|
||||
elementId = ensureBase64Ext(typeModel, elementId)
|
||||
|
@ -264,7 +264,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
// if the element already exists in the cache, overwrite it
|
||||
// add new element to existing list if necessary
|
||||
cache.elements.set(elementId, entity)
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
if (await this.isElementIdInCacheRange(typeRef, listId, customIdToBase64Url(typeModel, elementId))) {
|
||||
this.insertIntoRange(cache.allRange, elementId)
|
||||
}
|
||||
|
@ -309,7 +309,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
return null
|
||||
}
|
||||
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
return {
|
||||
lower: customIdToBase64Url(typeModel, listCache.lowerRangeId),
|
||||
upper: customIdToBase64Url(typeModel, listCache.upperRangeId),
|
||||
|
@ -317,7 +317,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
}
|
||||
|
||||
async setUpperRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, upperId: Id): Promise<void> {
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
upperId = ensureBase64Ext(typeModel, upperId)
|
||||
const listCache = this.lists.get(getTypeString(typeRef))?.get(listId)
|
||||
if (listCache == null) {
|
||||
|
@ -327,7 +327,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
}
|
||||
|
||||
async setLowerRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, lowerId: Id): Promise<void> {
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
lowerId = ensureBase64Ext(typeModel, lowerId)
|
||||
const listCache = this.lists.get(getTypeString(typeRef))?.get(listId)
|
||||
if (listCache == null) {
|
||||
|
@ -344,7 +344,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
* @param upper
|
||||
*/
|
||||
async setNewRangeForList<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id, lower: Id, upper: Id): Promise<void> {
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
lower = ensureBase64Ext(typeModel, lower)
|
||||
upper = ensureBase64Ext(typeModel, upper)
|
||||
|
||||
|
@ -365,7 +365,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
}
|
||||
|
||||
async getIdsInRange<T extends ListElementEntity>(typeRef: TypeRef<T>, listId: Id): Promise<Array<Id>> {
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
return (
|
||||
this.lists
|
||||
.get(getTypeString(typeRef))
|
||||
|
@ -412,7 +412,7 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
async deleteAllOwnedBy(owner: Id): Promise<void> {
|
||||
for (const [typeString, typeMap] of this.entities.entries()) {
|
||||
const typeRef = parseTypeString(typeString)
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
for (const [id, entity] of typeMap.entries()) {
|
||||
const ownerGroup = AttributeModel.getAttribute<Id>(entity, "_ownerGroup", typeModel)
|
||||
if (ownerGroup === owner) {
|
||||
|
@ -422,12 +422,12 @@ export class EphemeralCacheStorage implements CacheStorage {
|
|||
}
|
||||
for (const [typeString, cacheForType] of this.lists.entries()) {
|
||||
const typeRef = parseTypeString(typeString)
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
this.deleteAllOwnedByFromCache(typeModel, cacheForType, owner)
|
||||
}
|
||||
for (const [typeString, cacheForType] of this.blobEntities.entries()) {
|
||||
const typeRef = parseTypeString(typeString)
|
||||
const typeModel = await resolveServerTypeReference(typeRef)
|
||||
const typeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
this.deleteAllOwnedByFromCache(typeModel, cacheForType, owner)
|
||||
}
|
||||
this.lastBatchIdPerGroup.delete(owner)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { HttpMethod, MediaType, resolveClientTypeReference, resolveServerTypeReference } from "../../common/EntityFunctions"
|
||||
import { HttpMethod, MediaType, TypeModelResolver } from "../../common/EntityFunctions"
|
||||
import {
|
||||
DeleteService,
|
||||
ExtraServiceParams,
|
||||
|
@ -33,6 +33,7 @@ export class ServiceExecutor implements IServiceExecutor {
|
|||
private readonly authDataProvider: AuthDataProvider,
|
||||
private readonly instancePipeline: InstancePipeline,
|
||||
private readonly cryptoFacade: lazy<CryptoFacade>,
|
||||
private readonly typeModelResolver: TypeModelResolver,
|
||||
) {}
|
||||
|
||||
get<S extends GetService>(
|
||||
|
@ -77,7 +78,7 @@ export class ServiceExecutor implements IServiceExecutor {
|
|||
if (
|
||||
methodDefinition.return &&
|
||||
params?.sessionKey == null &&
|
||||
(await resolveClientTypeReference(methodDefinition.return)).encrypted &&
|
||||
(await this.typeModelResolver.resolveClientTypeReference(methodDefinition.return)).encrypted &&
|
||||
!this.authDataProvider.isFullyLoggedIn()
|
||||
) {
|
||||
// Short-circuit before we do an actual request which we can't decrypt
|
||||
|
@ -126,7 +127,7 @@ export class ServiceExecutor implements IServiceExecutor {
|
|||
if (someTypeRef == null) {
|
||||
throw new ProgrammingError("Need either data or return for the service method!")
|
||||
}
|
||||
const model = await resolveClientTypeReference(someTypeRef)
|
||||
const model = await this.typeModelResolver.resolveClientTypeReference(someTypeRef)
|
||||
return model.version
|
||||
}
|
||||
|
||||
|
@ -142,7 +143,7 @@ export class ServiceExecutor implements IServiceExecutor {
|
|||
throw new ProgrammingError(`Invalid service data! ${service.name} ${method}`)
|
||||
}
|
||||
|
||||
const requestTypeModel = await resolveClientTypeReference(methodDefinition.data)
|
||||
const requestTypeModel = await this.typeModelResolver.resolveClientTypeReference(methodDefinition.data)
|
||||
if (requestTypeModel.encrypted && params?.sessionKey == null) {
|
||||
throw new ProgrammingError("Must provide a session key for an encrypted data transfer type!: " + service)
|
||||
}
|
||||
|
@ -158,7 +159,7 @@ export class ServiceExecutor implements IServiceExecutor {
|
|||
private async decryptResponse<T extends Entity>(typeRef: TypeRef<T>, data: string, params: ExtraServiceParams | undefined): Promise<T> {
|
||||
// Filter out __proto__ to avoid prototype pollution.
|
||||
const instance: ServerModelUntypedInstance = JSON.parse(data, (k, v) => (k === "__proto__" ? undefined : v))
|
||||
const serverTypeModel = await resolveServerTypeReference(typeRef)
|
||||
const serverTypeModel = await this.typeModelResolver.resolveServerTypeReference(typeRef)
|
||||
const cleanInstance = AttributeModel.removeNetworkDebuggingInfoIfNeeded<ServerModelUntypedInstance>(instance)
|
||||
const encryptedParsedInstance = await this.instancePipeline.typeMapper.applyJsTypes(serverTypeModel, cleanInstance)
|
||||
const entityAdapter = await EntityAdapter.from(serverTypeModel, encryptedParsedInstance, this.instancePipeline)
|
||||
|
|
|
@ -77,7 +77,7 @@ import { MailboxExportPersistence } from "./export/MailboxExportPersistence.js"
|
|||
import { DesktopExportLock } from "./export/DesktopExportLock"
|
||||
import { ProgrammingError } from "../api/common/error/ProgrammingError"
|
||||
import { InstancePipeline } from "../api/worker/crypto/InstancePipeline"
|
||||
import { resolveClientTypeReference } from "../api/common/EntityFunctions"
|
||||
import { ClientModelInfo } from "../api/common/EntityFunctions"
|
||||
|
||||
mp()
|
||||
|
||||
|
@ -165,11 +165,15 @@ async function createComponents(): Promise<Components> {
|
|||
const tray = new DesktopTray(conf)
|
||||
const notifier = new DesktopNotifier(tray, new ElectronNotificationFactory())
|
||||
const dateProvider = new DefaultDateProvider()
|
||||
const clientModelInfo = ClientModelInfo.getInstance()
|
||||
// We need a custom instance pipeline for everything native as we only process them with the client type model
|
||||
// When upgrading things in SseFacade and AlarmStorage, we need to deprecate the old clients potentially
|
||||
const nativeInstancePipeline = new InstancePipeline(resolveClientTypeReference, resolveClientTypeReference)
|
||||
const nativeInstancePipeline = new InstancePipeline(
|
||||
clientModelInfo.resolveClientTypeReference.bind(clientModelInfo),
|
||||
clientModelInfo.resolveClientTypeReference.bind(clientModelInfo),
|
||||
)
|
||||
const sseStorage = new SseStorage(conf)
|
||||
const alarmStorage = new DesktopAlarmStorage(conf, desktopCrypto, keyStoreFacade, nativeInstancePipeline)
|
||||
const alarmStorage = new DesktopAlarmStorage(conf, desktopCrypto, keyStoreFacade, nativeInstancePipeline, clientModelInfo)
|
||||
const updater = new ElectronUpdater(conf, notifier, desktopCrypto, app, appIcon, new UpdaterWrapper(), fs)
|
||||
const shortcutManager = new LocalShortcutManager()
|
||||
const credentialsDb = new DesktopCredentialsStorage(__NODE_GYP_better_sqlite3, makeDbPath("credentials"), app)
|
||||
|
@ -253,6 +257,7 @@ async function createComponents(): Promise<Components> {
|
|||
suspensionAwareFetch,
|
||||
app.getVersion(),
|
||||
nativeInstancePipeline,
|
||||
clientModelInfo,
|
||||
)
|
||||
const sseClient = new SseClient(desktopNet, new DesktopSseDelay(), schedulerImpl)
|
||||
const sse = new TutaSseFacade(
|
||||
|
@ -265,6 +270,7 @@ async function createComponents(): Promise<Components> {
|
|||
suspensionAwareFetch,
|
||||
dateProvider,
|
||||
nativeInstancePipeline,
|
||||
clientModelInfo,
|
||||
)
|
||||
// It should be ok to await this, all we are waiting for is dynamic imports
|
||||
const integrator = await getDesktopIntegratorForPlatform(electron, fs, child_process, () => import("winreg"))
|
||||
|
|
|
@ -13,6 +13,7 @@ import { hasError } from "../../api/common/utils/ErrorUtils"
|
|||
import { CryptoError } from "@tutao/tutanota-crypto/error.js"
|
||||
import { EncryptedAlarmNotification } from "../../native/common/EncryptedAlarmNotification"
|
||||
import { AttributeModel } from "../../api/common/AttributeModel"
|
||||
import { ClientTypeModelResolver, TypeModelResolver } from "../../api/common/EntityFunctions"
|
||||
|
||||
/**
|
||||
* manages session keys used for decrypting alarm notifications, encrypting & persisting them to disk
|
||||
|
@ -26,6 +27,7 @@ export class DesktopAlarmStorage {
|
|||
private readonly cryptoFacade: DesktopNativeCryptoFacade,
|
||||
private readonly keyStoreFacade: DesktopKeyStoreFacade,
|
||||
private readonly alarmStorageInstancePipeline: InstancePipeline,
|
||||
private readonly typeModelResolver: ClientTypeModelResolver,
|
||||
) {
|
||||
this.unencryptedSessionKeys = {}
|
||||
}
|
||||
|
@ -191,7 +193,7 @@ export class DesktopAlarmStorage {
|
|||
}
|
||||
|
||||
public async decryptAlarmNotification(an: ClientModelUntypedInstance): Promise<AlarmNotification> {
|
||||
const encryptedAlarmNotification = await EncryptedAlarmNotification.from(an as unknown as ServerModelUntypedInstance)
|
||||
const encryptedAlarmNotification = await EncryptedAlarmNotification.from(an as unknown as ServerModelUntypedInstance, this.typeModelResolver)
|
||||
for (const currentNotificationSessionKey of encryptedAlarmNotification.getNotificationSessionKeys()) {
|
||||
const pushIdentifierSessionKey = await this.getPushIdentifierSessionKey(currentNotificationSessionKey.pushIdentifier)
|
||||
|
||||
|
|
|
@ -15,11 +15,11 @@ import { DesktopAlarmStorage } from "./DesktopAlarmStorage.js"
|
|||
import { SseInfo } from "./SseInfo.js"
|
||||
import { SseStorage } from "./SseStorage.js"
|
||||
import { FetchImpl } from "../net/NetAgent"
|
||||
import { resolveClientTypeReference } from "../../api/common/EntityFunctions"
|
||||
import { StrippedEntity } from "../../api/common/utils/EntityUtils"
|
||||
import { ClientModelUntypedInstance, EncryptedParsedInstance, ServerModelUntypedInstance, TypeModel } from "../../api/common/EntityTypes"
|
||||
import { EncryptedParsedInstance, ServerModelUntypedInstance, TypeModel } from "../../api/common/EntityTypes"
|
||||
import { AttributeModel } from "../../api/common/AttributeModel"
|
||||
import { InstancePipeline } from "../../api/worker/crypto/InstancePipeline"
|
||||
import { ClientTypeModelResolver, TypeModelResolver } from "../../api/common/EntityFunctions"
|
||||
|
||||
const TAG = "[notifications]"
|
||||
|
||||
|
@ -41,6 +41,7 @@ export class TutaNotificationHandler {
|
|||
private readonly fetch: FetchImpl,
|
||||
private readonly appVersion: string,
|
||||
private readonly nativeInstancePipeline: InstancePipeline,
|
||||
private readonly typeModelResolver: ClientTypeModelResolver,
|
||||
) {}
|
||||
|
||||
async onMailNotification(sseInfo: SseInfo, notificationInfo: StrippedEntity<NotificationInfo>) {
|
||||
|
@ -115,8 +116,8 @@ export class TutaNotificationHandler {
|
|||
|
||||
const parsedResponse = await response.json()
|
||||
|
||||
const mailModel = await resolveClientTypeReference(MailTypeRef)
|
||||
const mailAddressModel = await resolveClientTypeReference(MailAddressTypeRef)
|
||||
const mailModel = await this.typeModelResolver.resolveClientTypeReference(MailTypeRef)
|
||||
const mailAddressModel = await this.typeModelResolver.resolveClientTypeReference(MailAddressTypeRef)
|
||||
const mailEncryptedParsedInstance: EncryptedParsedInstance = await this.nativeInstancePipeline.typeMapper.applyJsTypes(
|
||||
mailModel,
|
||||
parsedResponse as ServerModelUntypedInstance,
|
||||
|
|
|
@ -27,6 +27,7 @@ import { CryptoError } from "@tutao/tutanota-crypto/error.js"
|
|||
import { hasError } from "../../api/common/utils/ErrorUtils"
|
||||
import { elementIdPart } from "../../api/common/utils/EntityUtils"
|
||||
import { AttributeModel } from "../../api/common/AttributeModel"
|
||||
import { ClientTypeModelResolver, TypeModelResolver } from "../../api/common/EntityFunctions"
|
||||
|
||||
const log = makeTaggedLogger("[SSEFacade]")
|
||||
|
||||
|
@ -45,6 +46,7 @@ export class TutaSseFacade implements SseEventHandler {
|
|||
private readonly fetch: FetchImpl,
|
||||
private readonly date: DateProvider,
|
||||
private readonly nativeInstancePipeline: InstancePipeline,
|
||||
private readonly typeModelResolver: ClientTypeModelResolver,
|
||||
) {
|
||||
sseClient.setEventListener(this)
|
||||
}
|
||||
|
@ -147,7 +149,7 @@ export class TutaSseFacade implements SseEventHandler {
|
|||
// VisibleForTesting
|
||||
async handleAlarmNotification(encryptedMissedNotification: EncryptedMissedNotification) {
|
||||
for (const alarmNotificationUntyped of encryptedMissedNotification.alarmNotifications) {
|
||||
const encryptedAlarmNotification = await EncryptedAlarmNotification.from(alarmNotificationUntyped)
|
||||
const encryptedAlarmNotification = await EncryptedAlarmNotification.from(alarmNotificationUntyped, this.typeModelResolver)
|
||||
const alarmIdentifier = encryptedAlarmNotification.getAlarmId()
|
||||
const operation = downcast<OperationType>(encryptedAlarmNotification.getOperation())
|
||||
if (operation === OperationType.CREATE) {
|
||||
|
@ -204,7 +206,7 @@ export class TutaSseFacade implements SseEventHandler {
|
|||
} else {
|
||||
const untypedInstance = (await res.json()) as ServerModelUntypedInstance
|
||||
log.debug("downloaded missed notification")
|
||||
return await EncryptedMissedNotification.from(untypedInstance)
|
||||
return await EncryptedMissedNotification.from(untypedInstance, this.typeModelResolver)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import { SuspensionBehavior } from "../api/worker/rest/RestClient"
|
|||
import { DateProvider } from "../api/common/DateProvider.js"
|
||||
import { IServiceExecutor } from "../api/common/ServiceRequest"
|
||||
import { UsageTestAssignmentService, UsageTestParticipationService } from "../api/entities/usage/Services.js"
|
||||
import { resolveClientTypeReference } from "../api/common/EntityFunctions"
|
||||
import { lang, TranslationKey } from "./LanguageViewModel"
|
||||
import stream from "mithril/stream"
|
||||
import { Dialog, DialogType } from "../gui/base/Dialog"
|
||||
|
@ -28,6 +27,7 @@ import { EntityClient } from "../api/common/EntityClient.js"
|
|||
import { EventController } from "../api/main/EventController.js"
|
||||
import { createUserSettingsGroupRoot, UserSettingsGroupRootTypeRef } from "../api/entities/tutanota/TypeRefs.js"
|
||||
import { EntityUpdateData, isUpdateForTypeRef } from "../api/common/utils/EntityUpdateUtils.js"
|
||||
import { ClientTypeModelResolver, TypeModelResolver } from "../api/common/EntityFunctions"
|
||||
|
||||
const PRESELECTED_LIKERT_VALUE = null
|
||||
|
||||
|
@ -170,6 +170,7 @@ export class UsageTestModel implements PingAdapter {
|
|||
private readonly loginController: LoginController,
|
||||
private readonly eventController: EventController,
|
||||
private readonly usageTestController: () => UsageTestController,
|
||||
private readonly typeModelResolver: ClientTypeModelResolver,
|
||||
) {
|
||||
eventController.addEntityListener((updates: ReadonlyArray<EntityUpdateData>) => {
|
||||
return this.entityEventsReceived(updates)
|
||||
|
@ -304,7 +305,7 @@ export class UsageTestModel implements PingAdapter {
|
|||
}
|
||||
|
||||
private async modelVersion(): Promise<number> {
|
||||
const model = await resolveClientTypeReference(UsageTestAssignmentTypeRef)
|
||||
const model = await this.typeModelResolver.resolveClientTypeReference(UsageTestAssignmentTypeRef)
|
||||
return model.version
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { ServerModelUntypedInstance, TypeModel, UntypedInstance } from "../../api/common/EntityTypes"
|
||||
import { resolveClientTypeReference } from "../../api/common/EntityFunctions"
|
||||
import {
|
||||
AlarmInfoTypeRef,
|
||||
AlarmNotificationTypeRef,
|
||||
|
@ -10,6 +9,7 @@ import {
|
|||
import { AttributeModel } from "../../api/common/AttributeModel"
|
||||
import { isSameId } from "../../api/common/utils/EntityUtils"
|
||||
import { assertNotNull, Base64, base64ToUint8Array } from "@tutao/tutanota-utils"
|
||||
import { ClientTypeModelResolver, TypeModelResolver } from "../../api/common/EntityFunctions"
|
||||
|
||||
export class EncryptedAlarmNotification {
|
||||
private constructor(
|
||||
|
@ -19,10 +19,10 @@ export class EncryptedAlarmNotification {
|
|||
private alarmInfoTypeModel: TypeModel,
|
||||
) {}
|
||||
|
||||
public static async from(untypedInstance: ServerModelUntypedInstance): Promise<EncryptedAlarmNotification> {
|
||||
const alarmNotificationTypeModel = await resolveClientTypeReference(AlarmNotificationTypeRef)
|
||||
const notificationSessionKeyTypeModel = await resolveClientTypeReference(NotificationSessionKeyTypeRef)
|
||||
const alarmInfoTypeModel = await resolveClientTypeReference(AlarmInfoTypeRef)
|
||||
public static async from(untypedInstance: ServerModelUntypedInstance, typeModelResolver: ClientTypeModelResolver): Promise<EncryptedAlarmNotification> {
|
||||
const alarmNotificationTypeModel = await typeModelResolver.resolveClientTypeReference(AlarmNotificationTypeRef)
|
||||
const notificationSessionKeyTypeModel = await typeModelResolver.resolveClientTypeReference(NotificationSessionKeyTypeRef)
|
||||
const alarmInfoTypeModel = await typeModelResolver.resolveClientTypeReference(AlarmInfoTypeRef)
|
||||
|
||||
const sanitizedUntypedInstance = await AttributeModel.removeNetworkDebuggingInfoIfNeeded<ServerModelUntypedInstance>(untypedInstance)
|
||||
return new EncryptedAlarmNotification(sanitizedUntypedInstance, alarmNotificationTypeModel, notificationSessionKeyTypeModel, alarmInfoTypeModel)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { ServerModelUntypedInstance, TypeModel } from "../../api/common/EntityTypes"
|
||||
import { resolveClientTypeReference } from "../../api/common/EntityFunctions"
|
||||
import {
|
||||
AlarmNotificationTypeRef,
|
||||
createNotificationSessionKey,
|
||||
|
@ -10,6 +9,7 @@ import {
|
|||
import { AttributeModel } from "../../api/common/AttributeModel"
|
||||
import { Base64, base64ToUint8Array } from "@tutao/tutanota-utils"
|
||||
import { Nullable } from "@tutao/tutanota-utils/dist/Utils"
|
||||
import { ClientTypeModelResolver } from "../../api/common/EntityFunctions"
|
||||
|
||||
export class EncryptedMissedNotification {
|
||||
private constructor(
|
||||
|
@ -19,10 +19,10 @@ export class EncryptedMissedNotification {
|
|||
private readonly notificationSessionKeyTypeModel: TypeModel,
|
||||
) {}
|
||||
|
||||
public static async from(untypedInstance: ServerModelUntypedInstance): Promise<EncryptedMissedNotification> {
|
||||
const missedNotificationTypeModel = await resolveClientTypeReference(MissedNotificationTypeRef)
|
||||
const alarmNotificationTypeModel = await resolveClientTypeReference(AlarmNotificationTypeRef)
|
||||
const notificationSessionKeyTypeModel = await resolveClientTypeReference(NotificationSessionKeyTypeRef)
|
||||
public static async from(untypedInstance: ServerModelUntypedInstance, typeModelResolver: ClientTypeModelResolver): Promise<EncryptedMissedNotification> {
|
||||
const missedNotificationTypeModel = await typeModelResolver.resolveClientTypeReference(MissedNotificationTypeRef)
|
||||
const alarmNotificationTypeModel = await typeModelResolver.resolveClientTypeReference(AlarmNotificationTypeRef)
|
||||
const notificationSessionKeyTypeModel = await typeModelResolver.resolveClientTypeReference(NotificationSessionKeyTypeRef)
|
||||
|
||||
const sanitizedUntypedInstance = await AttributeModel.removeNetworkDebuggingInfoIfNeeded<ServerModelUntypedInstance>(untypedInstance)
|
||||
|
||||
|
|
|
@ -673,9 +673,7 @@ export class MailViewModel {
|
|||
instanceId: elementIdPart(importedMailSetEntry._id),
|
||||
instanceListId: importedFolder.entries,
|
||||
operation: OperationType.CREATE,
|
||||
typeId: MailSetEntryTypeRef.typeId,
|
||||
type: "MailSetEntry",
|
||||
application: MailSetEntryTypeRef.app,
|
||||
typeRef: MailSetEntryTypeRef,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -135,6 +135,7 @@ import { BulkMailLoader } from "./workerUtils/index/BulkMailLoader.js"
|
|||
import { MailExportFacade } from "../common/api/worker/facades/lazy/MailExportFacade.js"
|
||||
import { SyncTracker } from "../common/api/main/SyncTracker.js"
|
||||
import { getEventWithDefaultTimes, setNextHalfHour } from "../common/api/common/utils/CommonCalendarUtils.js"
|
||||
import { ClientModelInfo, ClientTypeModelResolver, ServerModelInfo, TypeModelResolver } from "../common/api/common/EntityFunctions"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
|
@ -203,6 +204,10 @@ class MailLocator {
|
|||
private entropyFacade!: EntropyFacade
|
||||
private sqlCipherFacade!: SqlCipherFacade
|
||||
|
||||
readonly typeModelResolver: lazy<ClientTypeModelResolver> = lazyMemoized(() => {
|
||||
return ClientModelInfo.getInstance()
|
||||
})
|
||||
|
||||
readonly recipientsModel: lazyAsync<RecipientsModel> = lazyMemoized(async () => {
|
||||
const { RecipientsModel } = await import("../common/api/main/RecipientsModel.js")
|
||||
return new RecipientsModel(this.contactModel, this.logins, this.mailFacade, this.entityClient, this.keyVerificationFacade)
|
||||
|
@ -763,7 +768,7 @@ class MailLocator {
|
|||
this.progressTracker = new ProgressTracker()
|
||||
this.syncTracker = new SyncTracker()
|
||||
this.search = new SearchModel(this.searchFacade, () => this.calendarEventsRepository())
|
||||
this.entityClient = new EntityClient(restInterface)
|
||||
this.entityClient = new EntityClient(restInterface, this.typeModelResolver())
|
||||
this.cryptoFacade = cryptoFacade
|
||||
this.cacheStorage = cacheStorage
|
||||
this.entropyFacade = entropyFacade
|
||||
|
@ -805,6 +810,7 @@ class MailLocator {
|
|||
this.logins,
|
||||
this.eventController,
|
||||
() => this.usageTestController,
|
||||
this.typeModelResolver(),
|
||||
)
|
||||
this.usageTestController = new UsageTestController(this.usageTestModel)
|
||||
this.Const = Const
|
||||
|
|
|
@ -82,7 +82,6 @@ import { YEAR_IN_MILLIS } from "@tutao/tutanota-utils/dist/DateUtils.js"
|
|||
import { ListFilter } from "../../../common/misc/ListModel"
|
||||
import { client } from "../../../common/misc/ClientDetector"
|
||||
import { AppName } from "@tutao/tutanota-utils/dist/TypeRef"
|
||||
import { resolveTypeRefFromAppAndTypeNameLegacy } from "../../../common/api/common/EntityFunctions"
|
||||
|
||||
const SEARCH_PAGE_SIZE = 100
|
||||
|
||||
|
@ -774,9 +773,7 @@ export class SearchViewModel {
|
|||
|
||||
const { instanceListId, instanceId, operation } = update
|
||||
const id = [neverNull(instanceListId), instanceId] as const
|
||||
const typeRef = update.typeId
|
||||
? new TypeRef<SomeEntity>(update.application as AppName, update.typeId)
|
||||
: resolveTypeRefFromAppAndTypeNameLegacy(update.application as AppName, update.type)
|
||||
const typeRef = update.typeRef
|
||||
|
||||
if (!this.isInSearchResult(typeRef, id) && !isPossibleABirthdayContactUpdate) {
|
||||
return
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { EntityUpdate } from "../../../common/api/entities/sys/TypeRefs.js"
|
|||
import { EntityClient } from "../../../common/api/common/EntityClient.js"
|
||||
import { GroupDataOS, MetaDataOS } from "../../../common/api/worker/search/IndexTables.js"
|
||||
import { AttributeModel } from "../../../common/api/common/AttributeModel"
|
||||
import { EntityUpdateData } from "../../../common/api/common/utils/EntityUpdateUtils"
|
||||
|
||||
export class ContactIndexer {
|
||||
_core: IndexerCore
|
||||
|
@ -83,7 +84,7 @@ export class ContactIndexer {
|
|||
return tokenize(contact.firstName + " " + contact.lastName + " " + contact.mailAddresses.map((ma) => ma.address).join(" "))
|
||||
}
|
||||
|
||||
processNewContact(event: EntityUpdate): Promise<
|
||||
processNewContact(event: EntityUpdateData): Promise<
|
||||
| {
|
||||
contact: Contact
|
||||
keyToIndexEntries: Map<string, SearchIndexEntry[]>
|
||||
|
@ -156,7 +157,7 @@ export class ContactIndexer {
|
|||
}
|
||||
}
|
||||
|
||||
processEntityEvents(events: EntityUpdate[], groupId: Id, batchId: Id, indexUpdate: IndexUpdate): Promise<void> {
|
||||
processEntityEvents(events: EntityUpdateData[], groupId: Id, batchId: Id, indexUpdate: IndexUpdate): Promise<void> {
|
||||
return promiseMap(events, async (event) => {
|
||||
if (event.operation === OperationType.CREATE) {
|
||||
await this.processNewContact(event).then((result) => {
|
||||
|
|
|
@ -81,7 +81,8 @@ import {
|
|||
import { KeyLoaderFacade } from "../../../common/api/worker/facades/KeyLoaderFacade.js"
|
||||
import { getIndexerMetaData, updateEncryptionMetadata } from "../../../common/api/worker/facades/lazy/ConfigurationDatabase.js"
|
||||
import { encryptKeyWithVersionedKey, VersionedKey } from "../../../common/api/worker/crypto/CryptoWrapper.js"
|
||||
import { isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils"
|
||||
import { EntityUpdateData, entityUpdateToUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils"
|
||||
import { ClientTypeModelResolver, TypeModelResolver } from "../../../common/api/common/EntityFunctions"
|
||||
|
||||
export type InitParams = {
|
||||
user: User
|
||||
|
@ -149,6 +150,7 @@ export class Indexer {
|
|||
browserData: BrowserData,
|
||||
defaultEntityRestCache: DefaultEntityRestCache,
|
||||
makeMailIndexer: (core: IndexerCore, db: Db) => MailIndexer,
|
||||
private readonly typeModelResolver: ClientTypeModelResolver,
|
||||
) {
|
||||
let deferred = defer<void>()
|
||||
this._dbInitializedDeferredObject = deferred
|
||||
|
@ -161,8 +163,8 @@ export class Indexer {
|
|||
// correctly initialized during init()
|
||||
this._core = new IndexerCore(this.db, new EventQueue("indexer_core", true, (batch) => this._processEntityEvents(batch)), browserData)
|
||||
this._entityRestClient = entityRestClient
|
||||
this._entity = new EntityClient(defaultEntityRestCache)
|
||||
this._contact = new ContactIndexer(this._core, this.db, this._entity, new SuggestionFacade(ContactTypeRef, this.db))
|
||||
this._entity = new EntityClient(defaultEntityRestCache, typeModelResolver)
|
||||
this._contact = new ContactIndexer(this._core, this.db, this._entity, new SuggestionFacade(ContactTypeRef, this.db, typeModelResolver))
|
||||
this._mail = makeMailIndexer(this._core, this.db)
|
||||
this._indexedGroupIds = []
|
||||
this._initiallyLoadedBatchIdsPerGroup = new Map()
|
||||
|
@ -303,8 +305,8 @@ export class Indexer {
|
|||
return this._mail.cancelMailIndexing()
|
||||
}
|
||||
|
||||
addBatchesToQueue(batches: QueuedBatch[]) {
|
||||
this._realtimeEventQueue.addBatches(batches)
|
||||
addBatchesToQueue(events: readonly EntityUpdateData[], batchId: Id, groupId: Id) {
|
||||
this._realtimeEventQueue.addBatches([{ batchId, groupId, events: events.slice() }])
|
||||
}
|
||||
|
||||
startProcessing() {
|
||||
|
@ -546,12 +548,12 @@ export class Indexer {
|
|||
|
||||
for (let batch of eventBatchesOnServer) {
|
||||
const batchId = getElementId(batch)
|
||||
|
||||
const updatesArray = await promiseMap(batch.events, (event) => entityUpdateToUpdateData(this.typeModelResolver, event))
|
||||
if (groupIdToEventBatch.eventBatchIds.indexOf(batchId) === -1 && firstBiggerThanSecond(batchId, startId)) {
|
||||
batchesToQueue.push({
|
||||
groupId: groupIdToEventBatch.groupId,
|
||||
batchId,
|
||||
events: batch.events,
|
||||
events: updatesArray,
|
||||
})
|
||||
const lastBatch = lastLoadedBatchIdInGroup.get(groupIdToEventBatch.groupId)
|
||||
|
||||
|
@ -652,7 +654,7 @@ export class Indexer {
|
|||
})
|
||||
}
|
||||
|
||||
_processEntityEvents(batch: QueuedBatch): Promise<any> {
|
||||
async _processEntityEvents(batch: QueuedBatch): Promise<any> {
|
||||
const { events, groupId, batchId } = batch
|
||||
return this.db.initialized
|
||||
.then(async () => {
|
||||
|
@ -673,7 +675,7 @@ export class Indexer {
|
|||
}
|
||||
|
||||
markStart("processEntityEvents")
|
||||
const groupedEvents: Map<TypeRef<any>, EntityUpdate[]> = new Map() // define map first because Webstorm has problems with type annotations
|
||||
const groupedEvents: Map<TypeRef<any>, EntityUpdateData[]> = new Map() // define map first because Webstorm has problems with type annotations
|
||||
|
||||
events.reduce((all, update) => {
|
||||
if (isUpdateForTypeRef(MailTypeRef, update)) {
|
||||
|
@ -749,7 +751,7 @@ export class Indexer {
|
|||
* @VisibleForTesting
|
||||
* @param events
|
||||
*/
|
||||
async _processUserEntityEvents(events: EntityUpdate[]): Promise<void> {
|
||||
async _processUserEntityEvents(events: EntityUpdateData[]): Promise<void> {
|
||||
for (const event of events) {
|
||||
if (!(event.operation === OperationType.UPDATE && isSameId(this._initParams.user._id, event.instanceId))) {
|
||||
continue
|
||||
|
|
|
@ -72,7 +72,7 @@ import {
|
|||
} from "../../../common/api/worker/search/IndexTables.js"
|
||||
import { AppName } from "@tutao/tutanota-utils/dist/TypeRef"
|
||||
import { SomeEntity } from "../../../common/api/common/EntityTypes"
|
||||
import { resolveTypeRefFromAppAndTypeNameLegacy } from "../../../common/api/common/EntityFunctions"
|
||||
import { EntityUpdateData } from "../../../common/api/common/utils/EntityUpdateUtils"
|
||||
|
||||
const SEARCH_INDEX_ROW_LENGTH = 1000
|
||||
|
||||
|
@ -198,12 +198,10 @@ export class IndexerCore {
|
|||
/**
|
||||
* Process delete event before applying to the index.
|
||||
*/
|
||||
async _processDeleted(event: EntityUpdate, indexUpdate: IndexUpdate): Promise<void> {
|
||||
async _processDeleted(event: EntityUpdateData, indexUpdate: IndexUpdate): Promise<void> {
|
||||
const encInstanceIdPlain = encryptIndexKeyUint8Array(this.db.key, event.instanceId, this.db.iv)
|
||||
const encInstanceIdB64 = uint8ArrayToBase64(encInstanceIdPlain)
|
||||
const typeRef = event.typeId
|
||||
? new TypeRef<SomeEntity>(event.application as AppName, parseInt(event.typeId))
|
||||
: resolveTypeRefFromAppAndTypeNameLegacy(event.application as AppName, event.type)
|
||||
const typeRef = event.typeRef
|
||||
|
||||
const { appId, typeId } = typeRefToTypeInfo(typeRef)
|
||||
const transaction = await this.db.dbFacade.createTransaction(true, [ElementDataOS])
|
||||
|
|
|
@ -241,7 +241,7 @@ export class MailIndexer {
|
|||
}
|
||||
|
||||
/** @private visibleForTesting */
|
||||
processMovedMail(event: EntityUpdate, indexUpdate: IndexUpdate): Promise<void> {
|
||||
processMovedMail(event: EntityUpdateData, indexUpdate: IndexUpdate): Promise<void> {
|
||||
let encInstanceId = encryptIndexKeyBase64(this._db.key, event.instanceId, this._db.iv)
|
||||
return this._db.dbFacade.createTransaction(true, [ElementDataOS]).then((transaction) => {
|
||||
return transaction.get(ElementDataOS, encInstanceId).then((elementData) => {
|
||||
|
@ -616,7 +616,7 @@ export class MailIndexer {
|
|||
})
|
||||
}
|
||||
|
||||
async processImportStateEntityEvents(events: EntityUpdate[], groupId: Id, batchId: Id, indexUpdate: IndexUpdate): Promise<void> {
|
||||
async processImportStateEntityEvents(events: EntityUpdateData[], groupId: Id, batchId: Id, indexUpdate: IndexUpdate): Promise<void> {
|
||||
if (!this.mailIndexingEnabled) return Promise.resolve()
|
||||
await promiseMap(events, async (event) => {
|
||||
// we can only process create and update events (create is because of EntityEvent optimization
|
||||
|
@ -683,7 +683,7 @@ export class MailIndexer {
|
|||
* @param batchId
|
||||
* @param indexUpdate which will be populated with operations
|
||||
*/
|
||||
processEntityEvents(events: EntityUpdate[], groupId: Id, batchId: Id, indexUpdate: IndexUpdate): Promise<void> {
|
||||
processEntityEvents(events: EntityUpdateData[], groupId: Id, batchId: Id, indexUpdate: IndexUpdate): Promise<void> {
|
||||
if (!this.mailIndexingEnabled) return Promise.resolve()
|
||||
return promiseMap(events, (event) => {
|
||||
const mailId: IdTuple = [event.instanceListId, event.instanceId]
|
||||
|
@ -720,13 +720,7 @@ export class MailIndexer {
|
|||
})
|
||||
.catch(ofClass(NotFoundError, () => console.log("tried to index update event for non existing mail")))
|
||||
} else if (event.operation === OperationType.DELETE) {
|
||||
if (
|
||||
!containsEventOfType(
|
||||
events.map((e) => entityUpdateToUpdateData(e)),
|
||||
OperationType.CREATE,
|
||||
event.instanceId,
|
||||
)
|
||||
) {
|
||||
if (!containsEventOfType(events, OperationType.CREATE, event.instanceId)) {
|
||||
// Check that this is *not* a move event. Move events are handled separately.
|
||||
return this._core._processDeleted(event, indexUpdate)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { MailTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { DbTransaction } from "../../../common/api/worker/search/DbFacade.js"
|
||||
import { resolveClientTypeReference } from "../../../common/api/common/EntityFunctions.js"
|
||||
import {
|
||||
arrayHash,
|
||||
asyncFind,
|
||||
|
@ -60,6 +59,7 @@ import type { TypeModel } from "../../../common/api/common/EntityTypes.js"
|
|||
import { EntityClient } from "../../../common/api/common/EntityClient.js"
|
||||
import { UserFacade } from "../../../common/api/worker/facades/UserFacade.js"
|
||||
import { ElementDataOS, SearchIndexMetaDataOS, SearchIndexOS, SearchIndexWordsIndex } from "../../../common/api/worker/search/IndexTables.js"
|
||||
import { ClientTypeModelResolver, TypeModelResolver } from "../../../common/api/common/EntityFunctions"
|
||||
|
||||
type RowsToReadForIndexKey = {
|
||||
indexKey: string
|
||||
|
@ -76,6 +76,7 @@ export class SearchFacade {
|
|||
private readonly suggestionFacades: SuggestionFacade<any>[],
|
||||
browserData: BrowserData,
|
||||
private readonly entityClient: EntityClient,
|
||||
private readonly typeModelResolver: ClientTypeModelResolver,
|
||||
) {
|
||||
this.promiseMapCompat = promiseMapCompat(browserData.needsMicrotaskHack)
|
||||
}
|
||||
|
@ -149,7 +150,7 @@ export class SearchFacade {
|
|||
|
||||
private async loadAndReduce(restriction: SearchRestriction, result: SearchResult, suggestionToken: string, minSuggestionCount: number): Promise<void> {
|
||||
if (result.results.length > 0) {
|
||||
const model = await resolveClientTypeReference(restriction.type)
|
||||
const model = await this.typeModelResolver.resolveClientTypeReference(restriction.type)
|
||||
// if we want the exact search order we try to find the complete sequence of words in an attribute of the instance.
|
||||
// for other cases we only check that an attribute contains a word that starts with suggestion word
|
||||
const suggestionQuery = result.matchWordOrder ? normalizeQuery(result.query) : suggestionToken
|
||||
|
@ -214,7 +215,7 @@ export class SearchFacade {
|
|||
const modelAssociation = model.associations[attributeId]
|
||||
if (modelAssociation && modelAssociation.type === AssociationType.Aggregation && entity[modelAssociation.name]) {
|
||||
let aggregates = modelAssociation.cardinality === Cardinality.Any ? entity[modelAssociation.name] : [entity[modelAssociation.name]]
|
||||
const refModel = await resolveClientTypeReference(new TypeRef(model.app, modelAssociation.refTypeId))
|
||||
const refModel = await this.typeModelResolver.resolveClientTypeReference(new TypeRef(model.app, modelAssociation.refTypeId))
|
||||
return asyncFind(aggregates, (aggregate) => {
|
||||
return this.containsSuggestionToken(downcast<Record<string, any>>(aggregate), refModel, null, suggestionToken, matchWordOrder)
|
||||
}).then((found) => found != null)
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { Db } from "../../../common/api/worker/search/SearchTypes.js"
|
|||
import { stringToUtf8Uint8Array, TypeRef, utf8Uint8ArrayToString } from "@tutao/tutanota-utils"
|
||||
import { aes256EncryptSearchIndexEntry, unauthenticatedAesDecrypt } from "@tutao/tutanota-crypto"
|
||||
import { SearchTermSuggestionsOS } from "../../../common/api/worker/search/IndexTables.js"
|
||||
import { resolveClientTypeReference } from "../../../common/api/common/EntityFunctions"
|
||||
import { ClientTypeModelResolver, TypeModelResolver } from "../../../common/api/common/EntityFunctions"
|
||||
|
||||
export type SuggestionsType = Record<string, string[]>
|
||||
|
||||
|
@ -11,7 +11,7 @@ export class SuggestionFacade<T> {
|
|||
type: TypeRef<T>
|
||||
_suggestions: SuggestionsType
|
||||
|
||||
constructor(type: TypeRef<T>, db: Db) {
|
||||
constructor(type: TypeRef<T>, db: Db, private readonly typeModelResolver: ClientTypeModelResolver) {
|
||||
this.type = type
|
||||
this._db = db
|
||||
this._suggestions = {}
|
||||
|
@ -20,7 +20,7 @@ export class SuggestionFacade<T> {
|
|||
load(): Promise<void> {
|
||||
return this._db.initialized.then(() => {
|
||||
return this._db.dbFacade.createTransaction(true, [SearchTermSuggestionsOS]).then(async (t) => {
|
||||
const typeName = (await resolveClientTypeReference(new TypeRef(this.type.app, this.type.typeId))).name.toLowerCase()
|
||||
const typeName = (await this.typeModelResolver.resolveClientTypeReference(new TypeRef(this.type.app, this.type.typeId))).name.toLowerCase()
|
||||
return t.get(SearchTermSuggestionsOS, typeName).then((encSuggestions) => {
|
||||
if (encSuggestions) {
|
||||
this._suggestions = JSON.parse(utf8Uint8ArrayToString(unauthenticatedAesDecrypt(this._db.key, encSuggestions, true)))
|
||||
|
@ -69,7 +69,7 @@ export class SuggestionFacade<T> {
|
|||
store(): Promise<void> {
|
||||
return this._db.initialized.then(() => {
|
||||
return this._db.dbFacade.createTransaction(false, [SearchTermSuggestionsOS]).then(async (t) => {
|
||||
const typeName = (await resolveClientTypeReference(new TypeRef(this.type.app, this.type.typeId))).name.toLowerCase()
|
||||
const typeName = (await this.typeModelResolver.resolveClientTypeReference(new TypeRef(this.type.app, this.type.typeId))).name.toLowerCase()
|
||||
let encSuggestions = aes256EncryptSearchIndexEntry(this._db.key, stringToUtf8Uint8Array(JSON.stringify(this._suggestions)))
|
||||
t.put(SearchTermSuggestionsOS, typeName, encSuggestions)
|
||||
return t.wait()
|
||||
|
|
|
@ -49,12 +49,6 @@ import type { BlobFacade } from "../../../common/api/worker/facades/lazy/BlobFac
|
|||
import { UserFacade } from "../../../common/api/worker/facades/UserFacade.js"
|
||||
import { OfflineStorage } from "../../../common/api/worker/offline/OfflineStorage.js"
|
||||
import { OFFLINE_STORAGE_MIGRATIONS, OfflineStorageMigrator } from "../../../common/api/worker/offline/OfflineStorageMigrator.js"
|
||||
import {
|
||||
globalClientModelInfo,
|
||||
resolveClientTypeReference,
|
||||
resolveServerTypeReference,
|
||||
globalServerModelInfo,
|
||||
} from "../../../common/api/common/EntityFunctions.js"
|
||||
import { FileFacadeSendDispatcher } from "../../../common/native/common/generatedipc/FileFacadeSendDispatcher.js"
|
||||
import { NativePushFacadeSendDispatcher } from "../../../common/native/common/generatedipc/NativePushFacadeSendDispatcher.js"
|
||||
import { NativeCryptoFacadeSendDispatcher } from "../../../common/native/common/generatedipc/NativeCryptoFacadeSendDispatcher.js"
|
||||
|
@ -87,7 +81,6 @@ import { CryptoWrapper } from "../../../common/api/worker/crypto/CryptoWrapper.j
|
|||
import { RecoverCodeFacade } from "../../../common/api/worker/facades/lazy/RecoverCodeFacade.js"
|
||||
import { CacheManagementFacade } from "../../../common/api/worker/facades/lazy/CacheManagementFacade.js"
|
||||
import { MailOfflineCleaner } from "../offline/MailOfflineCleaner.js"
|
||||
import type { QueuedBatch } from "../../../common/api/worker/EventQueue.js"
|
||||
import { Credentials } from "../../../common/misc/credentials/Credentials.js"
|
||||
import { AsymmetricCryptoFacade } from "../../../common/api/worker/crypto/AsymmetricCryptoFacade.js"
|
||||
import { KeyVerificationFacade } from "../../../common/api/worker/facades/lazy/KeyVerificationFacade"
|
||||
|
@ -99,6 +92,7 @@ import { BulkMailLoader } from "../index/BulkMailLoader.js"
|
|||
import type { MailExportFacade } from "../../../common/api/worker/facades/lazy/MailExportFacade"
|
||||
import { InstancePipeline } from "../../../common/api/worker/crypto/InstancePipeline"
|
||||
import { ApplicationTypesFacade } from "../../../common/api/worker/facades/ApplicationTypesFacade"
|
||||
import { ClientModelInfo, ServerModelInfo, TypeModelResolver } from "../../../common/api/common/EntityFunctions"
|
||||
|
||||
assertWorkerOrNode()
|
||||
|
||||
|
@ -190,13 +184,26 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
|
|||
const domainConfig = new DomainConfigProvider().getCurrentDomainConfig()
|
||||
locator.restClient = new RestClient(suspensionHandler, domainConfig, () => locator.applicationTypesFacade)
|
||||
|
||||
locator.instancePipeline = new InstancePipeline(resolveClientTypeReference, resolveServerTypeReference)
|
||||
locator.serviceExecutor = new ServiceExecutor(locator.restClient, locator.user, locator.instancePipeline, () => locator.crypto)
|
||||
locator.applicationTypesFacade = await ApplicationTypesFacade.getInitialized(locator.restClient, fileFacadeSendDispatcher, globalServerModelInfo)
|
||||
const clientModelInfo = ClientModelInfo.getInstance()
|
||||
const serverModelInfo = ServerModelInfo.getPossiblyUninitializedInstance(clientModelInfo)
|
||||
const typeModelResolver = new TypeModelResolver(clientModelInfo, serverModelInfo)
|
||||
locator.instancePipeline = new InstancePipeline(
|
||||
typeModelResolver.resolveClientTypeReference.bind(typeModelResolver),
|
||||
typeModelResolver.resolveServerTypeReference.bind(typeModelResolver),
|
||||
)
|
||||
locator.serviceExecutor = new ServiceExecutor(locator.restClient, locator.user, locator.instancePipeline, () => locator.crypto, typeModelResolver)
|
||||
locator.applicationTypesFacade = await ApplicationTypesFacade.getInitialized(locator.restClient, fileFacadeSendDispatcher, serverModelInfo)
|
||||
locator.entropyFacade = new EntropyFacade(locator.user, locator.serviceExecutor, random, () => locator.keyLoader)
|
||||
locator.blobAccessToken = new BlobAccessTokenFacade(locator.serviceExecutor, locator.user, dateProvider)
|
||||
locator.blobAccessToken = new BlobAccessTokenFacade(locator.serviceExecutor, locator.user, dateProvider, typeModelResolver)
|
||||
|
||||
const entityRestClient = new EntityRestClient(locator.user, locator.restClient, () => locator.crypto, locator.instancePipeline, locator.blobAccessToken)
|
||||
const entityRestClient = new EntityRestClient(
|
||||
locator.user,
|
||||
locator.restClient,
|
||||
() => locator.crypto,
|
||||
locator.instancePipeline,
|
||||
locator.blobAccessToken,
|
||||
typeModelResolver,
|
||||
)
|
||||
locator.native = worker
|
||||
|
||||
locator.booking = lazyMemoized(async () => {
|
||||
|
@ -214,6 +221,7 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
|
|||
new OfflineStorageMigrator(OFFLINE_STORAGE_MIGRATIONS),
|
||||
new MailOfflineCleaner(),
|
||||
locator.instancePipeline.modelMapper,
|
||||
typeModelResolver,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
@ -225,10 +233,10 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
|
|||
}
|
||||
|
||||
const maybeUninitializedStorage = new LateInitializedCacheStorageImpl(
|
||||
locator.instancePipeline.modelMapper,
|
||||
async (error: Error) => {
|
||||
await worker.sendError(error)
|
||||
},
|
||||
async () => new EphemeralCacheStorage(locator.instancePipeline.modelMapper, typeModelResolver),
|
||||
offlineStorageProvider,
|
||||
)
|
||||
|
||||
|
@ -237,13 +245,13 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
|
|||
// We don't want to cache within the admin client
|
||||
let cache: DefaultEntityRestCache | null = null
|
||||
if (!isAdminClient()) {
|
||||
cache = new DefaultEntityRestCache(entityRestClient, maybeUninitializedStorage)
|
||||
cache = new DefaultEntityRestCache(entityRestClient, maybeUninitializedStorage, typeModelResolver)
|
||||
}
|
||||
|
||||
locator.cache = cache ?? entityRestClient
|
||||
|
||||
locator.cachingEntityClient = new EntityClient(locator.cache)
|
||||
const nonCachingEntityClient = new EntityClient(entityRestClient)
|
||||
locator.cachingEntityClient = new EntityClient(locator.cache, typeModelResolver)
|
||||
const nonCachingEntityClient = new EntityClient(entityRestClient, typeModelResolver)
|
||||
|
||||
locator.cacheManagement = lazyMemoized(async () => {
|
||||
const { CacheManagementFacade } = await import("../../../common/api/worker/facades/lazy/CacheManagementFacade.js")
|
||||
|
@ -259,8 +267,11 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
|
|||
return new BulkMailLoader(locator.cachingEntityClient, locator.cachingEntityClient)
|
||||
} else {
|
||||
// On platforms without offline cache we use new ephemeral cache storage for mails only and uncached storage for the rest
|
||||
const cacheStorage = new EphemeralCacheStorage(locator.instancePipeline.modelMapper)
|
||||
return new BulkMailLoader(new EntityClient(new DefaultEntityRestCache(entityRestClient, cacheStorage)), new EntityClient(entityRestClient))
|
||||
const cacheStorage = new EphemeralCacheStorage(locator.instancePipeline.modelMapper, typeModelResolver)
|
||||
return new BulkMailLoader(
|
||||
new EntityClient(new DefaultEntityRestCache(entityRestClient, cacheStorage, typeModelResolver), typeModelResolver),
|
||||
new EntityClient(entityRestClient, typeModelResolver),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -274,10 +285,17 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
|
|||
const { MailIndexer } = await import("../index/MailIndexer.js")
|
||||
const mailFacade = await locator.mail()
|
||||
const bulkLoaderFactory = await prepareBulkLoaderFactory()
|
||||
return new Indexer(entityRestClient, mainInterface.infoMessageHandler, browserData, locator.cache as DefaultEntityRestCache, (core, db) => {
|
||||
const dateProvider = new LocalTimeDateProvider()
|
||||
return new MailIndexer(core, db, mainInterface.infoMessageHandler, bulkLoaderFactory, locator.cachingEntityClient, dateProvider, mailFacade)
|
||||
})
|
||||
return new Indexer(
|
||||
entityRestClient,
|
||||
mainInterface.infoMessageHandler,
|
||||
browserData,
|
||||
locator.cache as DefaultEntityRestCache,
|
||||
(core, db) => {
|
||||
const dateProvider = new LocalTimeDateProvider()
|
||||
return new MailIndexer(core, db, mainInterface.infoMessageHandler, bulkLoaderFactory, locator.cachingEntityClient, dateProvider, mailFacade)
|
||||
},
|
||||
typeModelResolver,
|
||||
)
|
||||
})
|
||||
|
||||
if (isIOSApp() || isAndroidApp()) {
|
||||
|
@ -313,13 +331,14 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
|
|||
locator.restClient,
|
||||
locator.serviceExecutor,
|
||||
locator.instancePipeline,
|
||||
new OwnerEncSessionKeysUpdateQueue(locator.user, locator.serviceExecutor),
|
||||
new OwnerEncSessionKeysUpdateQueue(locator.user, locator.serviceExecutor, typeModelResolver),
|
||||
cache,
|
||||
locator.keyLoader,
|
||||
locator.asymmetricCrypto,
|
||||
locator.keyVerification,
|
||||
locator.publicKeyProvider,
|
||||
lazyMemoized(() => locator.keyRotation),
|
||||
typeModelResolver,
|
||||
)
|
||||
|
||||
locator.recoverCode = lazyMemoized(async () => {
|
||||
|
@ -402,7 +421,7 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
|
|||
/**
|
||||
* we don't want to try to use the cache in the login facade, because it may not be available (when no user is logged in)
|
||||
*/
|
||||
new EntityClient(locator.cache),
|
||||
new EntityClient(locator.cache, typeModelResolver),
|
||||
loginListener,
|
||||
locator.instancePipeline,
|
||||
locator.crypto,
|
||||
|
@ -419,13 +438,14 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
|
|||
await worker.sendError(error)
|
||||
},
|
||||
locator.cacheManagement,
|
||||
typeModelResolver,
|
||||
)
|
||||
|
||||
locator.search = lazyMemoized(async () => {
|
||||
const { SearchFacade } = await import("../index/SearchFacade.js")
|
||||
const indexer = await locator.indexer()
|
||||
const suggestionFacades = [indexer._contact.suggestionFacade]
|
||||
return new SearchFacade(locator.user, indexer.db, indexer._mail, suggestionFacades, browserData, locator.cachingEntityClient)
|
||||
return new SearchFacade(locator.user, indexer.db, indexer._mail, suggestionFacades, browserData, locator.cachingEntityClient, typeModelResolver)
|
||||
})
|
||||
locator.userManagement = lazyMemoized(async () => {
|
||||
const { UserManagementFacade } = await import("../../../common/api/worker/facades/lazy/UserManagementFacade.js")
|
||||
|
@ -495,6 +515,7 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
|
|||
locator.crypto,
|
||||
mainInterface.infoMessageHandler,
|
||||
locator.instancePipeline,
|
||||
locator.cachingEntityClient,
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -526,9 +547,9 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
|
|||
async (error: Error) => {
|
||||
await worker.sendError(error)
|
||||
},
|
||||
async (queuedBatch: QueuedBatch[]) => {
|
||||
async (events, batchId, groupId) => {
|
||||
const indexer = await locator.indexer()
|
||||
indexer.addBatchesToQueue(queuedBatch)
|
||||
indexer.addBatchesToQueue(events, batchId, groupId)
|
||||
indexer.startProcessing()
|
||||
},
|
||||
)
|
||||
|
@ -544,6 +565,7 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
|
|||
mainInterface.progressTracker,
|
||||
mainInterface.syncTracker,
|
||||
locator.applicationTypesFacade,
|
||||
typeModelResolver,
|
||||
)
|
||||
locator.login.init(locator.eventBusClient)
|
||||
locator.Const = Const
|
||||
|
@ -553,7 +575,7 @@ export async function initLocator(worker: WorkerImpl, browserData: BrowserData)
|
|||
})
|
||||
locator.contactFacade = lazyMemoized(async () => {
|
||||
const { ContactFacade } = await import("../../../common/api/worker/facades/lazy/ContactFacade.js")
|
||||
return new ContactFacade(new EntityClient(locator.cache))
|
||||
return new ContactFacade(new EntityClient(locator.cache, typeModelResolver))
|
||||
})
|
||||
locator.mailExportFacade = lazyMemoized(async () => {
|
||||
const { MailExportFacade } = await import("../../../common/api/worker/facades/lazy/MailExportFacade.js")
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { Db } from "../../src/common/api/worker/search/SearchTypes.js"
|
|||
import { IndexerCore } from "../../src/mail-app/workerUtils/index/IndexerCore.js"
|
||||
import { EventQueue } from "../../src/common/api/worker/EventQueue.js"
|
||||
import { DbFacade, DbTransaction } from "../../src/common/api/worker/search/DbFacade.js"
|
||||
import { AppNameEnum, assertNotNull, deepEqual, defer, Thunk, TypeRef } from "@tutao/tutanota-utils"
|
||||
import { AppNameEnum, assertNotNull, clone, deepEqual, defer, Thunk, TypeRef } from "@tutao/tutanota-utils"
|
||||
import type { DesktopKeyStoreFacade } from "../../src/common/desktop/DesktopKeyStoreFacade.js"
|
||||
import { mock } from "@tutao/tutanota-test-utils"
|
||||
import { aes256RandomKey, fixedIv, uint8ArrayToKey } from "@tutao/tutanota-crypto"
|
||||
|
@ -11,9 +11,11 @@ import { ScheduledPeriodicId, ScheduledTimeoutId, Scheduler } from "../../src/co
|
|||
import { matchers, object, when } from "testdouble"
|
||||
import { Entity, ModelValue, TypeModel } from "../../src/common/api/common/EntityTypes.js"
|
||||
import { create } from "../../src/common/api/common/utils/EntityUtils.js"
|
||||
import { ClientModelInfo, ServerModelInfo } from "../../src/common/api/common/EntityFunctions.js"
|
||||
import { ClientModelInfo, ServerModelInfo, TypeModelResolver } from "../../src/common/api/common/EntityFunctions.js"
|
||||
import { type fetch as undiciFetch, type Response } from "undici"
|
||||
import { Cardinality, ValueType } from "../../src/common/api/common/EntityConstants.js"
|
||||
import { InstancePipeline } from "../../src/common/api/worker/crypto/InstancePipeline"
|
||||
import { ModelMapper } from "../../src/common/api/worker/crypto/ModelMapper"
|
||||
|
||||
export const browserDataStub: BrowserData = {
|
||||
needsMicrotaskHack: false,
|
||||
|
@ -143,7 +145,7 @@ export const domainConfigStub: DomainConfig = {
|
|||
|
||||
// non-async copy of the function
|
||||
function resolveTypeReference(typeRef: TypeRef<any>): TypeModel {
|
||||
const modelMap = new ClientModelInfo().typeModels[typeRef.app]
|
||||
const modelMap = ClientModelInfo.getNewInstanceForTestsOnly().typeModels[typeRef.app]
|
||||
const typeModel = modelMap[typeRef.typeId]
|
||||
|
||||
if (typeModel == null) {
|
||||
|
@ -272,7 +274,7 @@ export function clientModelAsServerModel(serverModel: ServerModelInfo, clientMod
|
|||
[app]: {
|
||||
name: app,
|
||||
version: clientModel.modelInfos[app].version,
|
||||
types: clientModel.typeModels[app],
|
||||
types: clone(clientModel.typeModels[app]),
|
||||
},
|
||||
})
|
||||
return obj
|
||||
|
@ -280,3 +282,39 @@ export function clientModelAsServerModel(serverModel: ServerModelInfo, clientMod
|
|||
|
||||
serverModel.init("some_dummy_hash", models)
|
||||
}
|
||||
|
||||
export function clientInitializedTypeModelResolver(): TypeModelResolver {
|
||||
const clientModelInfo = ClientModelInfo.getNewInstanceForTestsOnly()
|
||||
const serverModelInfo = ServerModelInfo.getUninitializedInstanceForTestsOnly(clientModelInfo)
|
||||
const typeModelResolver = new TypeModelResolver(clientModelInfo, serverModelInfo)
|
||||
clientModelAsServerModel(serverModelInfo, clientModelInfo)
|
||||
return typeModelResolver
|
||||
}
|
||||
|
||||
export function instancePipelineFromTypeModelResolver(typeModelResolver: TypeModelResolver): InstancePipeline {
|
||||
return new InstancePipeline(
|
||||
typeModelResolver.resolveClientTypeReference.bind(typeModelResolver),
|
||||
typeModelResolver.resolveServerTypeReference.bind(typeModelResolver),
|
||||
)
|
||||
}
|
||||
|
||||
export function modelMapperFromTypeModelResolver(typeModelResolver: TypeModelResolver): ModelMapper {
|
||||
return new ModelMapper(
|
||||
typeModelResolver.resolveClientTypeReference.bind(typeModelResolver),
|
||||
typeModelResolver.resolveServerTypeReference.bind(typeModelResolver),
|
||||
)
|
||||
}
|
||||
|
||||
export async function withOverriddenEnv<F extends (...args: any[]) => any>(override: Partial<typeof env>, action: () => ReturnType<F>) {
|
||||
const previousEnv: typeof env = clone(env)
|
||||
for (const [key, value] of Object.entries(override)) {
|
||||
env[key] = value
|
||||
}
|
||||
try {
|
||||
return await action()
|
||||
} finally {
|
||||
for (const key of Object.keys(override)) {
|
||||
env[key] = previousEnv[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import o from "@tutao/otest"
|
||||
import { ClientModelInfo, resolveTypeRefFromAppAndTypeNameLegacy, ServerModelInfo, ServerModels } from "../../../../src/common/api/common/EntityFunctions"
|
||||
import { ClientModelInfo, ServerModelInfo, ServerModels } from "../../../../src/common/api/common/EntityFunctions"
|
||||
import { AppName } from "@tutao/tutanota-utils/lib/TypeRef"
|
||||
import { stringToUtf8Uint8Array } from "@tutao/tutanota-utils"
|
||||
import { Cardinality, Type, ValueType } from "../../../../src/common/api/common/EntityConstants"
|
||||
|
@ -13,14 +13,12 @@ import { TypeModel } from "../../../../src/common/api/common/EntityTypes"
|
|||
o.spec("EntityFunctionsTest", function () {
|
||||
let serverModelInfo: ServerModelInfo
|
||||
let clientModelInfo: ClientModelInfo
|
||||
let emptyTypeModel: ServerModels
|
||||
let applicationTypesFacade: ApplicationTypesFacade
|
||||
|
||||
o.beforeEach(async () => {
|
||||
clientModelInfo = new ClientModelInfo()
|
||||
serverModelInfo = new ServerModelInfo(clientModelInfo)
|
||||
clientModelInfo = ClientModelInfo.getNewInstanceForTestsOnly()
|
||||
serverModelInfo = ServerModelInfo.getUninitializedInstanceForTestsOnly(clientModelInfo)
|
||||
clientModelAsServerModel(serverModelInfo, clientModelInfo)
|
||||
emptyTypeModel = {} as ServerModels
|
||||
applicationTypesFacade = await ApplicationTypesFacade.getInitialized(object(), object(), serverModelInfo)
|
||||
})
|
||||
|
||||
|
@ -85,9 +83,9 @@ o.spec("EntityFunctionsTest", function () {
|
|||
o("fail to parse if encrypted value is changed to unencrypted", async () => {
|
||||
const serverModel = Object.assign({}, serverModelInfo.typeModels, partialServerModel)
|
||||
const serverModelString = JSON.stringify({ base: serverModel })
|
||||
clientModelInfo = new ClientModelInfo()
|
||||
clientModelInfo = ClientModelInfo.getNewInstanceForTestsOnly()
|
||||
clientModelInfo.typeModels = Object.assign({}, clientModelInfo.typeModels, { base: clientModel })
|
||||
serverModelInfo = new ServerModelInfo(clientModelInfo)
|
||||
serverModelInfo = ServerModelInfo.getUninitializedInstanceForTestsOnly(clientModelInfo)
|
||||
|
||||
const applicationTypesHashTruncatedBase64 = applicationTypesFacade.computeApplicationTypesHash(stringToUtf8Uint8Array(serverModelString))
|
||||
const e = await assertThrows(ProgrammingError, async () => serverModelInfo.init(applicationTypesHashTruncatedBase64, serverModel))
|
||||
|
@ -97,7 +95,7 @@ o.spec("EntityFunctionsTest", function () {
|
|||
o("ignore non-existent typeValue on client", async () => {
|
||||
const serverModel = Object.assign({}, serverModelInfo.typeModels, partialServerModel)
|
||||
const serverModelString = JSON.stringify({ base: serverModel })
|
||||
clientModelInfo = new ClientModelInfo()
|
||||
clientModelInfo = ClientModelInfo.getNewInstanceForTestsOnly()
|
||||
clientModelInfo.typeModels = Object.assign({}, clientModelInfo.typeModels, {
|
||||
base: {
|
||||
"0": {
|
||||
|
@ -124,7 +122,7 @@ o.spec("EntityFunctionsTest", function () {
|
|||
},
|
||||
},
|
||||
})
|
||||
serverModelInfo = new ServerModelInfo(clientModelInfo)
|
||||
serverModelInfo = ServerModelInfo.getUninitializedInstanceForTestsOnly(clientModelInfo)
|
||||
const applicationTypesHashTruncatedBase64 = applicationTypesFacade.computeApplicationTypesHash(stringToUtf8Uint8Array(serverModelString))
|
||||
serverModelInfo.init(applicationTypesHashTruncatedBase64, serverModel)
|
||||
})
|
||||
|
@ -134,7 +132,7 @@ o.spec("EntityFunctionsTest", function () {
|
|||
const app = "tutanota" as AppName
|
||||
const mailTypeId = 97
|
||||
const mailTypeName = clientModelInfo.typeModels.tutanota[mailTypeId].name
|
||||
const mailTypeRef = resolveTypeRefFromAppAndTypeNameLegacy(app, mailTypeName)
|
||||
const mailTypeRef = ClientModelInfo.getNewInstanceForTestsOnly().resolveTypeRefFromAppAndTypeNameLegacy(app, mailTypeName)
|
||||
o(mailTypeId).equals(mailTypeRef.typeId)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -19,7 +19,7 @@ import { EntityRestClientMock } from "./rest/EntityRestClientMock.js"
|
|||
import { EntityClient } from "../../../../src/common/api/common/EntityClient.js"
|
||||
import { defer, noOp } from "@tutao/tutanota-utils"
|
||||
import { DefaultEntityRestCache } from "../../../../src/common/api/worker/rest/DefaultEntityRestCache.js"
|
||||
import { EventQueue, QueuedBatch } from "../../../../src/common/api/worker/EventQueue.js"
|
||||
import { EventQueue } from "../../../../src/common/api/worker/EventQueue.js"
|
||||
import { OutOfSyncError } from "../../../../src/common/api/common/error/OutOfSyncError.js"
|
||||
import { matchers, object, verify, when } from "testdouble"
|
||||
import { getElementId, timestampToGeneratedId } from "../../../../src/common/api/common/utils/EntityUtils.js"
|
||||
|
@ -27,16 +27,12 @@ import { SleepDetector } from "../../../../src/common/api/worker/utils/SleepDete
|
|||
import { WsConnectionState } from "../../../../src/common/api/main/WorkerClient.js"
|
||||
import { UserFacade } from "../../../../src/common/api/worker/facades/UserFacade"
|
||||
import { ExposedProgressTracker } from "../../../../src/common/api/main/ProgressTracker.js"
|
||||
import { clientModelAsServerModel, createTestEntity } from "../../TestUtils.js"
|
||||
import { clientInitializedTypeModelResolver, clientModelAsServerModel, createTestEntity, instancePipelineFromTypeModelResolver } from "../../TestUtils.js"
|
||||
import { SyncTracker } from "../../../../src/common/api/main/SyncTracker.js"
|
||||
import { InstancePipeline } from "../../../../src/common/api/worker/crypto/InstancePipeline"
|
||||
import {
|
||||
globalClientModelInfo,
|
||||
globalServerModelInfo,
|
||||
resolveClientTypeReference,
|
||||
resolveServerTypeReference,
|
||||
} from "../../../../src/common/api/common/EntityFunctions"
|
||||
import { ApplicationTypesFacade } from "../../../../src/common/api/worker/facades/ApplicationTypesFacade"
|
||||
import { ClientModelInfo, ServerModelInfo, TypeModelResolver } from "../../../../src/common/api/common/EntityFunctions"
|
||||
import { EntityUpdateData } from "../../../../src/common/api/common/utils/EntityUpdateUtils"
|
||||
|
||||
const { anything } = matchers
|
||||
|
||||
|
@ -54,10 +50,10 @@ o.spec("EventBusClientTest", function () {
|
|||
let instancePipeline: InstancePipeline
|
||||
let socketFactory: (path: string) => WebSocket
|
||||
let applicationTypesFacadeMock: ApplicationTypesFacade
|
||||
let typeModelResolver: TypeModelResolver
|
||||
let entityClient: EntityClient
|
||||
|
||||
function initEventBus() {
|
||||
const entityClient = new EntityClient(restClient)
|
||||
instancePipeline = new InstancePipeline(resolveClientTypeReference, resolveServerTypeReference)
|
||||
applicationTypesFacadeMock = object()
|
||||
|
||||
ebc = new EventBusClient(
|
||||
|
@ -71,6 +67,7 @@ o.spec("EventBusClientTest", function () {
|
|||
progressTrackerMock,
|
||||
syncTrackerMock,
|
||||
applicationTypesFacadeMock,
|
||||
typeModelResolver,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -93,8 +90,8 @@ o.spec("EventBusClientTest", function () {
|
|||
progressTrackerMock = object()
|
||||
syncTrackerMock = object()
|
||||
cacheMock = object({
|
||||
async entityEventsReceived(batch: QueuedBatch): Promise<Array<EntityUpdate>> {
|
||||
return batch.events.slice()
|
||||
async entityEventsReceived(events): Promise<ReadonlyArray<EntityUpdateData>> {
|
||||
return events.slice()
|
||||
},
|
||||
async getLastEntityEventBatchForGroup(groupId: Id): Promise<Id | null> {
|
||||
return null
|
||||
|
@ -112,7 +109,7 @@ o.spec("EventBusClientTest", function () {
|
|||
async isOutOfSync(): Promise<boolean> {
|
||||
return false
|
||||
},
|
||||
} as DefaultEntityRestCache)
|
||||
} as Partial<DefaultEntityRestCache> as DefaultEntityRestCache)
|
||||
|
||||
user = createTestEntity(UserTypeRef, {
|
||||
userGroup: createTestEntity(GroupMembershipTypeRef, {
|
||||
|
@ -131,8 +128,10 @@ o.spec("EventBusClientTest", function () {
|
|||
sleepDetector = object()
|
||||
socketFactory = () => socket
|
||||
|
||||
typeModelResolver = clientInitializedTypeModelResolver()
|
||||
entityClient = new EntityClient(restClient, typeModelResolver)
|
||||
instancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
|
||||
initEventBus()
|
||||
clientModelAsServerModel(globalServerModelInfo, globalClientModelInfo)
|
||||
})
|
||||
|
||||
o.spec("initEntityEvents ", function () {
|
||||
|
@ -147,10 +146,11 @@ o.spec("EventBusClientTest", function () {
|
|||
]
|
||||
})
|
||||
|
||||
const batchId = "-----------1"
|
||||
o("initial connect: when the cache is clean it downloads one batch and initializes cache", async function () {
|
||||
when(cacheMock.getLastEntityEventBatchForGroup(mailGroupId)).thenResolve(null)
|
||||
when(cacheMock.timeSinceLastSyncMs()).thenResolve(null)
|
||||
const batch = createTestEntity(EntityEventBatchTypeRef, { _id: [mailGroupId, "-----------1"] })
|
||||
const batch = createTestEntity(EntityEventBatchTypeRef, { _id: [mailGroupId, batchId] })
|
||||
restClient.addListInstances(batch)
|
||||
|
||||
await ebc.connect(ConnectMode.Initial)
|
||||
|
@ -172,19 +172,19 @@ o.spec("EventBusClientTest", function () {
|
|||
instanceId: "newBatchId",
|
||||
})
|
||||
const batch = createTestEntity(EntityEventBatchTypeRef, {
|
||||
_id: [mailGroupId, "-----------1"],
|
||||
_id: [mailGroupId, batchId],
|
||||
events: [update],
|
||||
})
|
||||
restClient.addListInstances(batch)
|
||||
const updateData: EntityUpdateData = {
|
||||
typeRef: MailTypeRef,
|
||||
operation: OperationType.CREATE,
|
||||
instanceId: update.instanceId,
|
||||
instanceListId: update.instanceListId,
|
||||
}
|
||||
|
||||
const eventsReceivedDefer = defer()
|
||||
when(
|
||||
cacheMock.entityEventsReceived({
|
||||
events: [update],
|
||||
batchId: getElementId(batch),
|
||||
groupId: mailGroupId,
|
||||
}),
|
||||
).thenDo(() => eventsReceivedDefer.resolve(undefined))
|
||||
when(cacheMock.entityEventsReceived([updateData], batchId, mailGroupId)).thenDo(() => eventsReceivedDefer.resolve(undefined))
|
||||
|
||||
await ebc.connect(ConnectMode.Initial)
|
||||
await socket.onopen?.(new Event("open"))
|
||||
|
@ -234,7 +234,7 @@ o.spec("EventBusClientTest", function () {
|
|||
|
||||
// Casting ot object here because promise stubber doesn't allow you to just return the promise
|
||||
// We never resolve the promise
|
||||
when(cacheMock.entityEventsReceived(matchers.anything()) as object).thenReturn(new Promise(noOp))
|
||||
when(cacheMock.entityEventsReceived(matchers.anything(), matchers.anything(), matchers.anything()) as object).thenReturn(new Promise(noOp))
|
||||
|
||||
// call twice as if it was received in parallel
|
||||
const p1 = socket.onmessage?.({
|
||||
|
@ -248,7 +248,7 @@ o.spec("EventBusClientTest", function () {
|
|||
await Promise.all([p1, p2])
|
||||
|
||||
// Is waiting for cache to process the first event
|
||||
verify(cacheMock.entityEventsReceived(matchers.anything()), { times: 1 })
|
||||
verify(cacheMock.entityEventsReceived(matchers.anything(), matchers.anything(), matchers.anything()), { times: 1 })
|
||||
})
|
||||
|
||||
o("missed entity events are processed in order", async function () {
|
||||
|
|
|
@ -2,7 +2,6 @@ import o from "@tutao/otest"
|
|||
import { EventBusEventCoordinator } from "../../../../src/common/api/worker/EventBusEventCoordinator.js"
|
||||
import { matchers, object, verify, when } from "testdouble"
|
||||
import {
|
||||
EntityUpdate,
|
||||
EntityUpdateTypeRef,
|
||||
GroupKeyUpdateTypeRef,
|
||||
GroupMembershipTypeRef,
|
||||
|
@ -11,7 +10,7 @@ import {
|
|||
UserTypeRef,
|
||||
WebsocketLeaderStatusTypeRef,
|
||||
} from "../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { createTestEntity } from "../../TestUtils.js"
|
||||
import { createTestEntity, withOverriddenEnv } from "../../TestUtils.js"
|
||||
import { AccountType, OperationType } from "../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import { UserFacade } from "../../../../src/common/api/worker/facades/UserFacade.js"
|
||||
import { EntityClient } from "../../../../src/common/api/common/EntityClient.js"
|
||||
|
@ -20,7 +19,8 @@ import { MailFacade } from "../../../../src/common/api/worker/facades/lazy/MailF
|
|||
import { EventController } from "../../../../src/common/api/main/EventController.js"
|
||||
import { KeyRotationFacade } from "../../../../src/common/api/worker/facades/KeyRotationFacade.js"
|
||||
import { CacheManagementFacade } from "../../../../src/common/api/worker/facades/lazy/CacheManagementFacade.js"
|
||||
import { QueuedBatch } from "../../../../src/common/api/worker/EventQueue.js"
|
||||
import { EntityUpdateData } from "../../../../src/common/api/common/utils/EntityUpdateUtils"
|
||||
import { Mode } from "../../../../src/common/api/common/Env"
|
||||
|
||||
o.spec("EventBusEventCoordinatorTest", () => {
|
||||
let eventBusEventCoordinator: EventBusEventCoordinator
|
||||
|
@ -58,24 +58,24 @@ o.spec("EventBusEventCoordinatorTest", () => {
|
|||
keyRotationFacadeMock,
|
||||
async () => cacheManagementFacade,
|
||||
async (error: Error) => {},
|
||||
(queuedBatch: QueuedBatch[]) => {},
|
||||
(_) => {},
|
||||
)
|
||||
})
|
||||
|
||||
o("updateUser and UserGroupKeyDistribution", async function () {
|
||||
const updates: Array<EntityUpdate> = [
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: UserTypeRef.app,
|
||||
typeId: UserTypeRef.typeId.toString(),
|
||||
const updates: Array<EntityUpdateData> = [
|
||||
{
|
||||
typeRef: UserTypeRef,
|
||||
instanceId: userId,
|
||||
instanceListId: "",
|
||||
operation: OperationType.UPDATE,
|
||||
}),
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: UserGroupKeyDistributionTypeRef.app,
|
||||
typeId: UserGroupKeyDistributionTypeRef.typeId.toString(),
|
||||
},
|
||||
{
|
||||
typeRef: UserGroupKeyDistributionTypeRef,
|
||||
instanceId: userGroupId,
|
||||
instanceListId: "",
|
||||
operation: OperationType.CREATE,
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
await eventBusEventCoordinator.onEntityEventsReceived(updates, "batchId", "groupId")
|
||||
|
@ -86,14 +86,14 @@ o.spec("EventBusEventCoordinatorTest", () => {
|
|||
verify(mailFacade.entityEventsReceived(updates))
|
||||
})
|
||||
|
||||
o("updatUser only user update", async function () {
|
||||
const updates: Array<EntityUpdate> = [
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: UserTypeRef.app,
|
||||
typeId: UserTypeRef.typeId.toString(),
|
||||
o("updateUser only user update", async function () {
|
||||
const updates: Array<EntityUpdateData> = [
|
||||
{
|
||||
typeRef: UserTypeRef,
|
||||
instanceId: userId,
|
||||
instanceListId: "",
|
||||
operation: OperationType.UPDATE,
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
await eventBusEventCoordinator.onEntityEventsReceived(updates, "batchId", "groupId")
|
||||
|
@ -107,14 +107,13 @@ o.spec("EventBusEventCoordinatorTest", () => {
|
|||
o("groupKeyUpdate", async function () {
|
||||
const instanceListId = "updateListId"
|
||||
const instanceId = "updateElementId"
|
||||
const updates: Array<EntityUpdate> = [
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: GroupKeyUpdateTypeRef.app,
|
||||
typeId: GroupKeyUpdateTypeRef.typeId.toString(),
|
||||
const updates: Array<EntityUpdateData> = [
|
||||
{
|
||||
typeRef: GroupKeyUpdateTypeRef,
|
||||
instanceListId,
|
||||
instanceId,
|
||||
operation: OperationType.CREATE,
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
await eventBusEventCoordinator.onEntityEventsReceived(updates, "batchId", "groupId")
|
||||
|
@ -127,31 +126,33 @@ o.spec("EventBusEventCoordinatorTest", () => {
|
|||
})
|
||||
|
||||
o.spec("onLeaderStatusChanged", function () {
|
||||
o("If we are not the leader client, delete the passphrase key", function () {
|
||||
env.mode = "Desktop"
|
||||
o("If we are not the leader client, delete the passphrase key", async function () {
|
||||
const leaderStatus = createTestEntity(WebsocketLeaderStatusTypeRef, { leaderStatus: false })
|
||||
|
||||
eventBusEventCoordinator.onLeaderStatusChanged(leaderStatus)
|
||||
await withOverriddenEnv({ mode: Mode.Desktop }, () => {
|
||||
eventBusEventCoordinator.onLeaderStatusChanged(leaderStatus)
|
||||
})
|
||||
|
||||
verify(keyRotationFacadeMock.reset())
|
||||
verify(keyRotationFacadeMock.processPendingKeyRotationsAndUpdates(matchers.anything()), { times: 0 })
|
||||
})
|
||||
|
||||
o("If we are the leader client of an internal user, execute key rotations", function () {
|
||||
env.mode = "Desktop"
|
||||
o("If we are the leader client of an internal user, execute key rotations", async function () {
|
||||
const leaderStatus = createTestEntity(WebsocketLeaderStatusTypeRef, { leaderStatus: true })
|
||||
|
||||
eventBusEventCoordinator.onLeaderStatusChanged(leaderStatus)
|
||||
await withOverriddenEnv({ mode: Mode.Desktop }, () => {
|
||||
eventBusEventCoordinator.onLeaderStatusChanged(leaderStatus)
|
||||
})
|
||||
|
||||
verify(keyRotationFacadeMock.processPendingKeyRotationsAndUpdates(user))
|
||||
})
|
||||
|
||||
o("If we are the leader client of an external user, delete the passphrase key", function () {
|
||||
env.mode = "Desktop"
|
||||
o("If we are the leader client of an external user, delete the passphrase key", async function () {
|
||||
const leaderStatus = createTestEntity(WebsocketLeaderStatusTypeRef, { leaderStatus: true })
|
||||
user.accountType = AccountType.EXTERNAL
|
||||
|
||||
eventBusEventCoordinator.onLeaderStatusChanged(leaderStatus)
|
||||
await withOverriddenEnv({ mode: Mode.Desktop }, () => {
|
||||
eventBusEventCoordinator.onLeaderStatusChanged(leaderStatus)
|
||||
})
|
||||
|
||||
verify(keyRotationFacadeMock.reset())
|
||||
verify(keyRotationFacadeMock.processPendingKeyRotationsAndUpdates(matchers.anything()), { times: 0 })
|
||||
|
|
|
@ -80,20 +80,14 @@ import { IServiceExecutor } from "../../../../../src/common/api/common/ServiceRe
|
|||
import { matchers, object, verify, when } from "testdouble"
|
||||
import { UpdatePermissionKeyService } from "../../../../../src/common/api/entities/sys/Services.js"
|
||||
import { elementIdPart, getListId, isSameId, listIdPart } from "../../../../../src/common/api/common/utils/EntityUtils.js"
|
||||
import {
|
||||
globalClientModelInfo,
|
||||
globalServerModelInfo,
|
||||
HttpMethod,
|
||||
resolveClientTypeReference,
|
||||
resolveServerTypeReference,
|
||||
} from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { ClientModelInfo, HttpMethod, ServerModelInfo, TypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { UserFacade } from "../../../../../src/common/api/worker/facades/UserFacade.js"
|
||||
import { SessionKeyNotFoundError } from "../../../../../src/common/api/common/error/SessionKeyNotFoundError.js"
|
||||
import { OwnerEncSessionKeysUpdateQueue } from "../../../../../src/common/api/worker/crypto/OwnerEncSessionKeysUpdateQueue.js"
|
||||
import { WASMKyberFacade } from "../../../../../src/common/api/worker/facades/KyberFacade.js"
|
||||
import { PQFacade } from "../../../../../src/common/api/worker/facades/PQFacade.js"
|
||||
import { encodePQMessage, PQBucketKeyEncapsulation } from "../../../../../src/common/api/worker/facades/PQMessage.js"
|
||||
import { clientModelAsServerModel, createTestEntity } from "../../../TestUtils.js"
|
||||
import { clientInitializedTypeModelResolver, clientModelAsServerModel, createTestEntity, instancePipelineFromTypeModelResolver } from "../../../TestUtils.js"
|
||||
import { RSA_TEST_KEYPAIR } from "../facades/RsaPqPerformanceTest.js"
|
||||
import { DefaultEntityRestCache } from "../../../../../src/common/api/worker/rest/DefaultEntityRestCache.js"
|
||||
import { loadLibOQSWASM } from "../WASMTestUtils.js"
|
||||
|
@ -128,69 +122,10 @@ type TestUser = {
|
|||
|
||||
const senderAddress = "hello@tutao.de"
|
||||
|
||||
async function prepareBucketKeyInstance(
|
||||
bucketEncMailSessionKey: Uint8Array,
|
||||
fileSessionKeys: Array<AesKey>,
|
||||
bk: AesKey,
|
||||
pubEncBucketKey: Uint8Array,
|
||||
recipientUser: TestUser,
|
||||
mail: Mail,
|
||||
senderPubEccKey: Versioned<X25519PublicKey> | undefined,
|
||||
recipientKeyVersion: NumberString,
|
||||
protocolVersion: CryptoProtocolVersion,
|
||||
asymmetricCryptoFacade: AsymmetricCryptoFacade,
|
||||
) {
|
||||
const MailTypeModel = await resolveClientTypeReference(MailTypeRef)
|
||||
|
||||
const mailInstanceSessionKey = createTestEntity(InstanceSessionKeyTypeRef, {
|
||||
typeInfo: createTestEntity(TypeInfoTypeRef, {
|
||||
application: MailTypeModel.app,
|
||||
typeId: String(MailTypeModel.id),
|
||||
}),
|
||||
symEncSessionKey: bucketEncMailSessionKey,
|
||||
instanceList: "mailListId",
|
||||
instanceId: "mailId",
|
||||
})
|
||||
const FileTypeModel = await resolveClientTypeReference(FileTypeRef)
|
||||
const bucketEncSessionKeys = fileSessionKeys.map((fileSessionKey, index) => {
|
||||
return createTestEntity(InstanceSessionKeyTypeRef, {
|
||||
typeInfo: createTestEntity(TypeInfoTypeRef, {
|
||||
application: FileTypeModel.app,
|
||||
typeId: String(FileTypeModel.id),
|
||||
}),
|
||||
symEncSessionKey: encryptKey(bk, fileSessionKey),
|
||||
instanceList: "fileListId",
|
||||
instanceId: "fileId" + (index + 1),
|
||||
})
|
||||
})
|
||||
bucketEncSessionKeys.push(mailInstanceSessionKey)
|
||||
|
||||
const bucketKey = createTestEntity(BucketKeyTypeRef, {
|
||||
pubEncBucketKey,
|
||||
keyGroup: recipientUser.userGroup._id,
|
||||
bucketEncSessionKeys: bucketEncSessionKeys,
|
||||
recipientKeyVersion,
|
||||
senderKeyVersion: senderPubEccKey != null ? senderPubEccKey.version.toString() : "0",
|
||||
protocolVersion,
|
||||
})
|
||||
|
||||
when(
|
||||
asymmetricCryptoFacade.loadKeyPairAndDecryptSymKey(
|
||||
assertNotNull(bucketKey.keyGroup),
|
||||
parseKeyVersion(bucketKey.recipientKeyVersion),
|
||||
asCryptoProtoocolVersion(bucketKey.protocolVersion),
|
||||
pubEncBucketKey,
|
||||
anything(),
|
||||
),
|
||||
).thenResolve({ decryptedAesKey: bk, senderIdentityPubKey: senderPubEccKey?.object ?? null })
|
||||
|
||||
mail.bucketKey = bucketKey
|
||||
}
|
||||
|
||||
o.spec("CryptoFacadeTest", function () {
|
||||
let restClient: RestClient
|
||||
|
||||
let instancePipeline = new InstancePipeline(resolveClientTypeReference, resolveServerTypeReference)
|
||||
let instancePipeline
|
||||
|
||||
let serviceExecutor: IServiceExecutor
|
||||
let entityClient: EntityClient
|
||||
|
@ -202,6 +137,66 @@ o.spec("CryptoFacadeTest", function () {
|
|||
let cache: DefaultEntityRestCache
|
||||
let asymmetricCryptoFacade: AsymmetricCryptoFacade
|
||||
let keyRotationFacade: KeyRotationFacade
|
||||
let typeModelResolver: TypeModelResolver
|
||||
|
||||
async function prepareBucketKeyInstance(
|
||||
bucketEncMailSessionKey: Uint8Array,
|
||||
fileSessionKeys: Array<AesKey>,
|
||||
bk: AesKey,
|
||||
pubEncBucketKey: Uint8Array,
|
||||
recipientUser: TestUser,
|
||||
mail: Mail,
|
||||
senderPubEccKey: Versioned<X25519PublicKey> | undefined,
|
||||
recipientKeyVersion: NumberString,
|
||||
protocolVersion: CryptoProtocolVersion,
|
||||
asymmetricCryptoFacade: AsymmetricCryptoFacade,
|
||||
) {
|
||||
const MailTypeModel = await typeModelResolver.resolveClientTypeReference(MailTypeRef)
|
||||
|
||||
const mailInstanceSessionKey = createTestEntity(InstanceSessionKeyTypeRef, {
|
||||
typeInfo: createTestEntity(TypeInfoTypeRef, {
|
||||
application: MailTypeModel.app,
|
||||
typeId: String(MailTypeModel.id),
|
||||
}),
|
||||
symEncSessionKey: bucketEncMailSessionKey,
|
||||
instanceList: "mailListId",
|
||||
instanceId: "mailId",
|
||||
})
|
||||
const FileTypeModel = await typeModelResolver.resolveClientTypeReference(FileTypeRef)
|
||||
const bucketEncSessionKeys = fileSessionKeys.map((fileSessionKey, index) => {
|
||||
return createTestEntity(InstanceSessionKeyTypeRef, {
|
||||
typeInfo: createTestEntity(TypeInfoTypeRef, {
|
||||
application: FileTypeModel.app,
|
||||
typeId: String(FileTypeModel.id),
|
||||
}),
|
||||
symEncSessionKey: encryptKey(bk, fileSessionKey),
|
||||
instanceList: "fileListId",
|
||||
instanceId: "fileId" + (index + 1),
|
||||
})
|
||||
})
|
||||
bucketEncSessionKeys.push(mailInstanceSessionKey)
|
||||
|
||||
const bucketKey = createTestEntity(BucketKeyTypeRef, {
|
||||
pubEncBucketKey,
|
||||
keyGroup: recipientUser.userGroup._id,
|
||||
bucketEncSessionKeys: bucketEncSessionKeys,
|
||||
recipientKeyVersion,
|
||||
senderKeyVersion: senderPubEccKey != null ? senderPubEccKey.version.toString() : "0",
|
||||
protocolVersion,
|
||||
})
|
||||
|
||||
when(
|
||||
asymmetricCryptoFacade.loadKeyPairAndDecryptSymKey(
|
||||
assertNotNull(bucketKey.keyGroup),
|
||||
parseKeyVersion(bucketKey.recipientKeyVersion),
|
||||
asCryptoProtoocolVersion(bucketKey.protocolVersion),
|
||||
pubEncBucketKey,
|
||||
anything(),
|
||||
),
|
||||
).thenResolve({ decryptedAesKey: bk, senderIdentityPubKey: senderPubEccKey?.object ?? null })
|
||||
|
||||
mail.bucketKey = bucketKey
|
||||
}
|
||||
|
||||
o.before(function () {
|
||||
restClient = object()
|
||||
|
@ -219,6 +214,9 @@ o.spec("CryptoFacadeTest", function () {
|
|||
publicKeyProvider = object()
|
||||
keyLoaderFacade = object()
|
||||
keyRotationFacade = object()
|
||||
typeModelResolver = clientInitializedTypeModelResolver()
|
||||
instancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
|
||||
|
||||
crypto = new CryptoFacade(
|
||||
userFacade,
|
||||
entityClient,
|
||||
|
@ -232,8 +230,8 @@ o.spec("CryptoFacadeTest", function () {
|
|||
async () => keyVerificationFacade,
|
||||
publicKeyProvider,
|
||||
() => keyRotationFacade,
|
||||
typeModelResolver,
|
||||
)
|
||||
clientModelAsServerModel(globalServerModelInfo, globalClientModelInfo)
|
||||
})
|
||||
|
||||
o("resolve session key: unencrypted instance", async function () {
|
||||
|
@ -1311,7 +1309,6 @@ o.spec("CryptoFacadeTest", function () {
|
|||
when(userFacade.hasGroup(ownerGroup)).thenReturn(true)
|
||||
when(userFacade.isFullyLoggedIn()).thenReturn(true)
|
||||
|
||||
const MailDetailsBlobTypeModel = await resolveClientTypeReference(MailDetailsBlobTypeRef)
|
||||
const mailDetailsBlob = createTestEntity(MailDetailsBlobTypeRef, {
|
||||
_id: ["mailDetailsArchiveId", "mailDetailsId"],
|
||||
_ownerGroup: ownerGroup,
|
||||
|
@ -1392,7 +1389,7 @@ o.spec("CryptoFacadeTest", function () {
|
|||
encryptionAuthStatus: null,
|
||||
symKeyVersion: "0",
|
||||
})
|
||||
const FileTypeModel = await resolveClientTypeReference(FileTypeRef)
|
||||
const FileTypeModel = await typeModelResolver.resolveClientTypeReference(FileTypeRef)
|
||||
const bucketEncSessionKeys = fileSessionKeys.map((fileSessionKey, index) => {
|
||||
return createInstanceSessionKey({
|
||||
typeInfo: createTypeInfo({
|
||||
|
@ -1587,7 +1584,7 @@ o.spec("CryptoFacadeTest", function () {
|
|||
const groupEncBucketKey = encryptKey(groupKeyToEncryptBucketKey, bk)
|
||||
const bucketEncMailSessionKey = encryptKey(bk, sk)
|
||||
|
||||
const MailTypeModel = await resolveServerTypeReference(MailTypeRef)
|
||||
const MailTypeModel = await typeModelResolver.resolveServerTypeReference(MailTypeRef)
|
||||
|
||||
const mailInstanceSessionKey = createTestEntity(InstanceSessionKeyTypeRef, {
|
||||
typeInfo: createTestEntity(TypeInfoTypeRef, {
|
||||
|
@ -1598,7 +1595,7 @@ o.spec("CryptoFacadeTest", function () {
|
|||
instanceList: "mailListId",
|
||||
instanceId: "mailId",
|
||||
})
|
||||
const FileTypeModel = await resolveServerTypeReference(FileTypeRef)
|
||||
const FileTypeModel = await typeModelResolver.resolveServerTypeReference(FileTypeRef)
|
||||
const bucketEncSessionKeys = fileSessionKeys.map((fileSessionKey, index) => {
|
||||
return createTestEntity(InstanceSessionKeyTypeRef, {
|
||||
typeInfo: createTestEntity(TypeInfoTypeRef, {
|
||||
|
@ -1685,7 +1682,7 @@ o.spec("CryptoFacadeTest", function () {
|
|||
const groupEncBucketKey = encryptKey(externalUser.mailGroupKey, bk)
|
||||
const bucketEncMailSessionKey = encryptKey(bk, sk)
|
||||
|
||||
const MailTypeModel = await resolveServerTypeReference(MailTypeRef)
|
||||
const MailTypeModel = await typeModelResolver.resolveServerTypeReference(MailTypeRef)
|
||||
const mailInstanceSessionKey = createTestEntity(InstanceSessionKeyTypeRef, {
|
||||
typeInfo: createTestEntity(TypeInfoTypeRef, {
|
||||
application: MailTypeModel.app,
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
import o from "@tutao/otest"
|
||||
import { resolveClientTypeReference, resolveServerTypeReference } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { ImportMailGetInTypeRef, MailAddressTypeRef, MailTypeRef } from "../../../../../src/common/api/entities/tutanota/TypeRefs"
|
||||
import { createTestEntity } from "../../../TestUtils"
|
||||
import { clientInitializedTypeModelResolver, createTestEntity, instancePipelineFromTypeModelResolver } from "../../../TestUtils"
|
||||
import { stringToUtf8Uint8Array } from "@tutao/tutanota-utils"
|
||||
import { BucketKey, BucketKeyTypeRef, GroupInfoTypeRef } from "../../../../../src/common/api/entities/sys/TypeRefs"
|
||||
import { EntityAdapter } from "../../../../../src/common/api/worker/crypto/EntityAdapter"
|
||||
import { InstancePipeline } from "../../../../../src/common/api/worker/crypto/InstancePipeline"
|
||||
import { assertThrows } from "@tutao/tutanota-test-utils"
|
||||
import { TypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
o.spec("EntityAdapter", () => {
|
||||
const instancePipeline = new InstancePipeline(resolveClientTypeReference, resolveServerTypeReference)
|
||||
let typeModelResolver: TypeModelResolver
|
||||
let instancePipeline: InstancePipeline
|
||||
|
||||
o.beforeEach(() => {
|
||||
typeModelResolver = clientInitializedTypeModelResolver()
|
||||
instancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
|
||||
})
|
||||
|
||||
o.test("can create local mapped/decrypted instance - GroupInfo", async () => {
|
||||
const groupModel = await resolveClientTypeReference(GroupInfoTypeRef)
|
||||
const groupModel = await typeModelResolver.resolveClientTypeReference(GroupInfoTypeRef)
|
||||
|
||||
const groupInfo = createTestEntity(GroupInfoTypeRef, {
|
||||
_ownerGroup: "ownerGroupId",
|
||||
|
@ -34,7 +40,7 @@ o.spec("EntityAdapter", () => {
|
|||
})
|
||||
|
||||
o.test("can create local mapped/decrypted instance - Mail", async () => {
|
||||
const mailModel = await resolveClientTypeReference(MailTypeRef)
|
||||
const mailModel = await typeModelResolver.resolveClientTypeReference(MailTypeRef)
|
||||
|
||||
const mail = createTestEntity(MailTypeRef, {
|
||||
_ownerGroup: "ownerGroupId",
|
||||
|
@ -61,7 +67,7 @@ o.spec("EntityAdapter", () => {
|
|||
})
|
||||
|
||||
o.test("can create local mapped/decrypted data transfer instance", async () => {
|
||||
const importMailGetInModel = await resolveClientTypeReference(ImportMailGetInTypeRef)
|
||||
const importMailGetInModel = await typeModelResolver.resolveClientTypeReference(ImportMailGetInTypeRef)
|
||||
|
||||
const importMailGetIn = createTestEntity(ImportMailGetInTypeRef, {
|
||||
ownerGroup: "ownerGroupId", // ownerGroupId is currently not used as MailGroup is hardcoded in CryptoFacade#resolveSessionKey
|
||||
|
@ -78,7 +84,7 @@ o.spec("EntityAdapter", () => {
|
|||
})
|
||||
|
||||
o.test("set _ownerEncSessionKey", async () => {
|
||||
const mailModel = await resolveClientTypeReference(MailTypeRef)
|
||||
const mailModel = await typeModelResolver.resolveClientTypeReference(MailTypeRef)
|
||||
|
||||
const mail = createTestEntity(MailTypeRef, {
|
||||
_permissions: "permissionListId",
|
||||
|
@ -102,7 +108,7 @@ o.spec("EntityAdapter", () => {
|
|||
})
|
||||
|
||||
o.test("set _ownerGroup", async () => {
|
||||
const mailModel = await resolveClientTypeReference(MailTypeRef)
|
||||
const mailModel = await typeModelResolver.resolveClientTypeReference(MailTypeRef)
|
||||
|
||||
const mail = createTestEntity(MailTypeRef, {
|
||||
_permissions: "permissionListId",
|
||||
|
|
|
@ -7,10 +7,10 @@ import { GroupKeyUpdateTypeRef, InstanceSessionKeyTypeRef, TypeInfoTypeRef } fro
|
|||
import { UpdateSessionKeysService } from "../../../../../src/common/api/entities/sys/Services.js"
|
||||
import { delay } from "@tutao/tutanota-utils"
|
||||
import { LockedError } from "../../../../../src/common/api/common/error/RestError.js"
|
||||
import { createTestEntity } from "../../../TestUtils.js"
|
||||
import { resolveClientTypeReference } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { clientInitializedTypeModelResolver, createTestEntity } from "../../../TestUtils.js"
|
||||
import { MailTypeRef } from "../../../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { TypeModel } from "../../../../../src/common/api/common/EntityTypes.js"
|
||||
import { TypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
const { anything, captor } = matchers
|
||||
|
||||
|
@ -19,13 +19,15 @@ o.spec("OwnerEncSessionKeysUpdateQueueTest", function () {
|
|||
let ownerEncSessionKeysUpdateQueue: OwnerEncSessionKeysUpdateQueue
|
||||
let userFacade: UserFacade
|
||||
let mailTypeModel: TypeModel
|
||||
let typeModelResolver: TypeModelResolver
|
||||
|
||||
o.beforeEach(async function () {
|
||||
mailTypeModel = await resolveClientTypeReference(MailTypeRef)
|
||||
typeModelResolver = clientInitializedTypeModelResolver()
|
||||
mailTypeModel = await typeModelResolver.resolveClientTypeReference(MailTypeRef)
|
||||
userFacade = object()
|
||||
when(userFacade.isLeader()).thenReturn(true)
|
||||
serviceExecutor = object()
|
||||
ownerEncSessionKeysUpdateQueue = new OwnerEncSessionKeysUpdateQueue(userFacade, serviceExecutor, 0)
|
||||
ownerEncSessionKeysUpdateQueue = new OwnerEncSessionKeysUpdateQueue(userFacade, serviceExecutor, typeModelResolver, 0)
|
||||
})
|
||||
|
||||
o.spec("updateInstanceSessionKeys", function () {
|
||||
|
@ -44,7 +46,7 @@ o.spec("OwnerEncSessionKeysUpdateQueueTest", function () {
|
|||
symEncSessionKey: new Uint8Array([4, 5, 6]),
|
||||
}),
|
||||
]
|
||||
await await ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(updatableInstanceSessionKeys, mailTypeModel)
|
||||
await ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(updatableInstanceSessionKeys, mailTypeModel)
|
||||
await delay(0)
|
||||
const updatedPostCaptor = captor()
|
||||
verify(serviceExecutor.post(UpdateSessionKeysService, updatedPostCaptor.capture()))
|
||||
|
@ -60,7 +62,7 @@ o.spec("OwnerEncSessionKeysUpdateQueueTest", function () {
|
|||
})
|
||||
|
||||
o("no updates sent for GroupKeyUpdate type", async function () {
|
||||
const groupKeyUpdateTypeModel = await resolveClientTypeReference(GroupKeyUpdateTypeRef)
|
||||
const groupKeyUpdateTypeModel = await typeModelResolver.resolveClientTypeReference(GroupKeyUpdateTypeRef)
|
||||
const updatableInstanceSessionKeys = [createTestEntity(InstanceSessionKeyTypeRef)]
|
||||
await ownerEncSessionKeysUpdateQueue.updateInstanceSessionKeys(updatableInstanceSessionKeys, groupKeyUpdateTypeModel)
|
||||
await delay(0)
|
||||
|
|
|
@ -3,26 +3,25 @@ import { ApplicationTypesFacade, ApplicationTypesGetOut } from "../../../../../s
|
|||
import { matchers, object, verify, when } from "testdouble"
|
||||
import { ApplicationTypesService } from "../../../../../src/common/api/entities/base/Services"
|
||||
import { AssociationType, Cardinality, Type } from "../../../../../src/common/api/common/EntityConstants"
|
||||
import { ClientModelInfo, HttpMethod, MediaType, ServerModelInfo, ServerModels } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { HttpMethod, MediaType, ServerModelInfo, ServerModels } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { Mode } from "../../../../../src/common/api/common/Env"
|
||||
import { AppName, AppNameEnum } from "@tutao/tutanota-utils/dist/TypeRef"
|
||||
import { ModelAssociation, ServerTypeModel, TypeModel } from "../../../../../src/common/api/common/EntityTypes"
|
||||
import { ModelAssociation, ServerTypeModel } from "../../../../../src/common/api/common/EntityTypes"
|
||||
import { downcast } from "@tutao/tutanota-utils"
|
||||
import { FileFacade } from "../../../../../src/common/native/common/generatedipc/FileFacade"
|
||||
import { RestClient } from "../../../../../src/common/api/worker/rest/RestClient"
|
||||
import { getServiceRestPath } from "../../../../../src/common/api/worker/rest/ServiceExecutor"
|
||||
import { ServiceDefinition } from "../../../../../src/common/api/common/ServiceRequest"
|
||||
import { compressString, decompressString } from "../../../../../src/common/api/worker/crypto/ModelMapper"
|
||||
import { withOverriddenEnv } from "../../../TestUtils"
|
||||
|
||||
const { anything } = matchers
|
||||
|
||||
o.spec("ApplicationTypesFacadeTest", function () {
|
||||
const initialMode = env.mode
|
||||
let restClient: RestClient
|
||||
let fileFacade: FileFacade
|
||||
let applicationTypesFacade: ApplicationTypesFacade
|
||||
let serverModelInfo: ServerModelInfo
|
||||
let clientModelInfo: ClientModelInfo
|
||||
let mockResponse = compressString(
|
||||
JSON.stringify({
|
||||
applicationTypesHash: "currentApplicationHash",
|
||||
|
@ -74,14 +73,9 @@ o.spec("ApplicationTypesFacadeTest", function () {
|
|||
restClient = object()
|
||||
fileFacade = object()
|
||||
serverModelInfo = object()
|
||||
clientModelInfo = object()
|
||||
applicationTypesFacade = await ApplicationTypesFacade.getInitialized(restClient, fileFacade, serverModelInfo)
|
||||
})
|
||||
|
||||
o.afterEach(function () {
|
||||
env.mode = initialMode
|
||||
})
|
||||
|
||||
o("getServerApplicationTypesJson does only one service request for requests made in quick succession", async function () {
|
||||
o.timeout(200)
|
||||
|
||||
|
@ -127,8 +121,6 @@ o.spec("ApplicationTypesFacadeTest", function () {
|
|||
}
|
||||
|
||||
o("server model should be assigned to memory first and write to file later", async () => {
|
||||
env.mode = "Desktop"
|
||||
|
||||
when(
|
||||
restClient.request(getServiceRestPath(ApplicationTypesService as ServiceDefinition), HttpMethod.GET, { responseType: MediaType.Binary }),
|
||||
).thenResolve(mockResponse)
|
||||
|
@ -138,13 +130,11 @@ o.spec("ApplicationTypesFacadeTest", function () {
|
|||
when(fileFacade.writeToAppDir(anything(), anything())).thenDo(async () => callOrder.push("write"))
|
||||
when(serverModelInfo.init(applicationTypesGetOut.applicationTypesHash, anything())).thenDo(() => callOrder.push("assign"))
|
||||
|
||||
await applicationTypesFacade.getServerApplicationTypesJson()
|
||||
await withOverriddenEnv({ mode: Mode.Desktop }, () => applicationTypesFacade.getServerApplicationTypesJson())
|
||||
o(callOrder).deepEquals(["assign", "write"])
|
||||
})
|
||||
|
||||
o("should attempt to write file but not propagate write error", async () => {
|
||||
env.mode = "Desktop"
|
||||
|
||||
when(
|
||||
restClient.request(getServiceRestPath(ApplicationTypesService as ServiceDefinition), HttpMethod.GET, { responseType: MediaType.Binary }),
|
||||
).thenResolve(mockResponse)
|
||||
|
@ -153,16 +143,15 @@ o.spec("ApplicationTypesFacadeTest", function () {
|
|||
when(serverModelInfo.init(applicationTypesGetOut.applicationTypesHash, anything())).thenReturn()
|
||||
when(fileFacade.writeToAppDir(anything(), anything())).thenReject(Error("writing failed simulation failed"))
|
||||
|
||||
await applicationTypesFacade.getServerApplicationTypesJson()
|
||||
await withOverriddenEnv({ mode: Mode.Desktop }, () => applicationTypesFacade.getServerApplicationTypesJson())
|
||||
|
||||
// verify that server model is updated even if writing to disk fails
|
||||
verify(serverModelInfo.init(anything(), anything()))
|
||||
})
|
||||
|
||||
o("should attempt to read but not fail on read error", async () => {
|
||||
env.mode = "Desktop"
|
||||
|
||||
when(fileFacade.readDataFile(anything())).thenReject(Error("reading failed simulation failed"))
|
||||
await ApplicationTypesFacade.getInitialized(object(), fileFacade, serverModelInfo)
|
||||
await withOverriddenEnv({ mode: Mode.Desktop }, () => ApplicationTypesFacade.getInitialized(object(), fileFacade, serverModelInfo))
|
||||
|
||||
// verify nothing changed in ServerModelInfo
|
||||
// did not throw
|
||||
|
@ -172,23 +161,19 @@ o.spec("ApplicationTypesFacadeTest", function () {
|
|||
const shouldPersist = ["Desktop", "App"].includes(targetEnv)
|
||||
|
||||
o(`Server model for should persist for native platforms: ${targetEnv}`, async () => {
|
||||
env.mode = targetEnv
|
||||
|
||||
when(
|
||||
restClient.request(getServiceRestPath(ApplicationTypesService as ServiceDefinition), HttpMethod.GET, { responseType: MediaType.Binary }),
|
||||
).thenResolve(mockResponse)
|
||||
when(serverModelInfo.init(anything(), anything())).thenResolve()
|
||||
when(fileFacade.writeToAppDir(anything(), anything())).thenReturn(Promise.resolve(downcast({})))
|
||||
|
||||
await applicationTypesFacade.getServerApplicationTypesJson()
|
||||
await withOverriddenEnv({ mode: targetEnv }, () => applicationTypesFacade.getServerApplicationTypesJson())
|
||||
|
||||
verify(fileFacade.writeToAppDir(anything(), anything()), { times: shouldPersist ? 1 : 0 })
|
||||
})
|
||||
|
||||
o(`Server model should be initialised from file for native platforms: ${targetEnv}`, async () => {
|
||||
env.mode = targetEnv
|
||||
|
||||
await ApplicationTypesFacade.getInitialized(object(), fileFacade, serverModelInfo)
|
||||
await withOverriddenEnv({ mode: targetEnv }, () => ApplicationTypesFacade.getInitialized(object(), fileFacade, serverModelInfo))
|
||||
verify(fileFacade.readFromAppDir(anything()), { times: shouldPersist ? 1 : 0 })
|
||||
})
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
BlobWriteDataTypeRef,
|
||||
InstanceIdTypeRef,
|
||||
} from "../../../../../src/common/api/entities/storage/TypeRefs.js"
|
||||
import { createTestEntity } from "../../../TestUtils.js"
|
||||
import { clientInitializedTypeModelResolver, createTestEntity } from "../../../TestUtils.js"
|
||||
import { FileTypeRef, MailBoxTypeRef } from "../../../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { BlobTypeRef } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { BlobReferencingInstance } from "../../../../../src/common/api/common/utils/BlobUtils.js"
|
||||
|
@ -45,11 +45,7 @@ o.spec("BlobAccessTokenFacade", function () {
|
|||
}
|
||||
serviceMock = object<ServiceExecutor>()
|
||||
authDataProvider = object<AuthDataProvider>()
|
||||
blobAccessTokenFacade = new BlobAccessTokenFacade(serviceMock, authDataProvider, dateProvider)
|
||||
})
|
||||
|
||||
o.afterEach(function () {
|
||||
env.mode = Mode.Browser
|
||||
blobAccessTokenFacade = new BlobAccessTokenFacade(serviceMock, authDataProvider, dateProvider, clientInitializedTypeModelResolver())
|
||||
})
|
||||
|
||||
o.spec("evict Tokens", function () {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { ArchiveDataType, MAX_BLOB_SIZE_BYTES } from "../../../../../src/common/
|
|||
import { BlobReferenceTokenWrapperTypeRef, BlobTypeRef } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { File as TutanotaFile, FileTypeRef } from "../../../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { instance, matchers, object, verify, when } from "testdouble"
|
||||
import { HttpMethod, resolveClientTypeReference, resolveServerTypeReference } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { HttpMethod } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { aes256RandomKey, aesDecrypt, aesEncrypt, generateIV } from "@tutao/tutanota-crypto"
|
||||
import { arrayEquals, base64ExtToBase64, base64ToUint8Array, concat, neverNull, stringToUtf8Uint8Array } from "@tutao/tutanota-utils"
|
||||
import { Mode } from "../../../../../src/common/api/common/Env.js"
|
||||
|
@ -25,7 +25,7 @@ import {
|
|||
} from "../../../../../src/common/api/entities/storage/TypeRefs.js"
|
||||
import { BlobAccessTokenFacade } from "../../../../../src/common/api/worker/facades/BlobAccessTokenFacade.js"
|
||||
import { elementIdPart, getElementId, listIdPart } from "../../../../../src/common/api/common/utils/EntityUtils.js"
|
||||
import { createTestEntity } from "../../../TestUtils.js"
|
||||
import { clientInitializedTypeModelResolver, createTestEntity, instancePipelineFromTypeModelResolver, withOverriddenEnv } from "../../../TestUtils.js"
|
||||
import { BlobReferencingInstance } from "../../../../../src/common/api/common/utils/BlobUtils.js"
|
||||
import { InstancePipeline } from "../../../../../src/common/api/worker/crypto/InstancePipeline"
|
||||
import { typeModels as storageTypeModels } from "../../../../../src/common/api/entities/storage/TypeModels"
|
||||
|
@ -80,7 +80,6 @@ o.spec("BlobFacade test", function () {
|
|||
})
|
||||
|
||||
o.afterEach(function () {
|
||||
env.mode = Mode.Browser
|
||||
env.networkDebugging = previousNetworkDebugging
|
||||
})
|
||||
|
||||
|
@ -88,7 +87,8 @@ o.spec("BlobFacade test", function () {
|
|||
o("parseBlobPostOutResponse should remove network debugging info", async function () {
|
||||
env.networkDebugging = true
|
||||
|
||||
const realInstancePipeline = new InstancePipeline(resolveClientTypeReference, resolveServerTypeReference)
|
||||
const typeModelResolver = clientInitializedTypeModelResolver()
|
||||
const realInstancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
|
||||
const newBlobFacade = new BlobFacade(
|
||||
restClientMock,
|
||||
suspensionHandlerMock,
|
||||
|
@ -165,9 +165,10 @@ o.spec("BlobFacade test", function () {
|
|||
responseBody: stringToUtf8Uint8Array(JSON.stringify(blobServiceResponse)),
|
||||
})
|
||||
|
||||
env.mode = Mode.Desktop
|
||||
env.versionNumber = "274.250306.0"
|
||||
const referenceTokens = await blobFacade.encryptAndUploadNative(archiveDataType, uploadedFileUri, ownerGroup, sessionKey)
|
||||
const referenceTokens = await withOverriddenEnv({ mode: Mode.Desktop }, () =>
|
||||
blobFacade.encryptAndUploadNative(archiveDataType, uploadedFileUri, ownerGroup, sessionKey),
|
||||
)
|
||||
|
||||
o(referenceTokens).deepEquals(expectedReferenceTokens)
|
||||
verify(
|
||||
|
|
|
@ -31,12 +31,12 @@ import { UserFacade } from "../../../../../src/common/api/worker/facades/UserFac
|
|||
import { InfoMessageHandler } from "../../../../../src/common/gui/InfoMessageHandler.js"
|
||||
import { ConnectionError } from "../../../../../src/common/api/common/error/RestError.js"
|
||||
import { EntityClient } from "../../../../../src/common/api/common/EntityClient.js"
|
||||
import { createTestEntity } from "../../../TestUtils.js"
|
||||
import { clientInitializedTypeModelResolver, createTestEntity, instancePipelineFromTypeModelResolver } from "../../../TestUtils.js"
|
||||
import { EntityRestClient } from "../../../../../src/common/api/worker/rest/EntityRestClient"
|
||||
import { InstancePipeline } from "../../../../../src/common/api/worker/crypto/InstancePipeline"
|
||||
import { resolveClientTypeReference, resolveServerTypeReference } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { uint8ArrayToBitArray } from "@tutao/tutanota-crypto"
|
||||
import { OperationType } from "../../../../../src/common/api/common/TutanotaConstants"
|
||||
import { TypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
o.spec("CalendarFacadeTest", function () {
|
||||
let userAlarmInfoListId: Id
|
||||
|
@ -57,6 +57,7 @@ o.spec("CalendarFacadeTest", function () {
|
|||
let serviceExecutor: IServiceExecutor
|
||||
let cryptoFacade: CryptoFacade
|
||||
let infoMessageHandler: InfoMessageHandler
|
||||
let typeModelResolver: TypeModelResolver
|
||||
let instancePipeline: InstancePipeline
|
||||
|
||||
function sortEventsWithAlarmInfos(eventsWithAlarmInfos: Array<EventWithUserAlarmInfos>) {
|
||||
|
@ -125,18 +126,20 @@ o.spec("CalendarFacadeTest", function () {
|
|||
serviceExecutor = object()
|
||||
cryptoFacade = object()
|
||||
infoMessageHandler = object()
|
||||
instancePipeline = new InstancePipeline(resolveClientTypeReference, resolveServerTypeReference)
|
||||
typeModelResolver = clientInitializedTypeModelResolver()
|
||||
instancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
|
||||
calendarFacade = new CalendarFacade(
|
||||
userFacade,
|
||||
groupManagementFacade,
|
||||
entityRestCache,
|
||||
new EntityClient(entityRestCache),
|
||||
new EntityClient(entityRestCache, typeModelResolver),
|
||||
nativeMock,
|
||||
workerMock,
|
||||
serviceExecutor,
|
||||
cryptoFacade,
|
||||
infoMessageHandler,
|
||||
instancePipeline,
|
||||
new EntityClient(entityRestCache, typeModelResolver),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import { KeyPairType, PQPublicKeys, PublicKey } from "@tutao/tutanota-crypto"
|
|||
import { PublicKeyIdentifier, PublicKeyProvider } from "../../../../../src/common/api/worker/facades/PublicKeyProvider"
|
||||
import { CustomerFacade } from "../../../../../src/common/api/worker/facades/lazy/CustomerFacade"
|
||||
import { Mode } from "../../../../../src/common/api/common/Env"
|
||||
import { withOverriddenEnv } from "../../../TestUtils"
|
||||
|
||||
const { anything } = matchers
|
||||
|
||||
|
@ -75,9 +76,6 @@ o.spec("KeyVerificationFacadeTest", function () {
|
|||
let backupEnv: any
|
||||
|
||||
o.beforeEach(function () {
|
||||
// Better safe than sorry.
|
||||
backupEnv = globalThis.env
|
||||
|
||||
customerFacade = object()
|
||||
sqlCipherFacade = object()
|
||||
publicKeyProvider = object()
|
||||
|
@ -90,10 +88,6 @@ o.spec("KeyVerificationFacadeTest", function () {
|
|||
when(publicKeyProvider.convertFromPublicKeyGetOut(PUBLIC_KEY_GET_OUT)).thenReturn(PUBLIC_KEY)
|
||||
})
|
||||
|
||||
o.afterEach(function () {
|
||||
globalThis.env = backupEnv
|
||||
})
|
||||
|
||||
o.spec("confirm trusted identity database works as intended", function () {
|
||||
o("identity database is empty", async function () {
|
||||
const sqlResult: Record<string, TaggedSqlValue>[] = []
|
||||
|
@ -353,26 +347,23 @@ o.spec("KeyVerificationFacadeTest", function () {
|
|||
})
|
||||
|
||||
o("feature should be supported when on desktop and enabled", async function () {
|
||||
globalThis.env.mode = Mode.Desktop
|
||||
when(customerFacade.isEnabled(FeatureType.KeyVerification)).thenResolve(true)
|
||||
|
||||
const isSupported = await keyVerification.isSupported()
|
||||
const isSupported = await withOverriddenEnv({ mode: Mode.Desktop }, () => keyVerification.isSupported())
|
||||
o(isSupported).equals(true)
|
||||
})
|
||||
|
||||
o("feature should NOT be supported when on desktop and disabled", async function () {
|
||||
globalThis.env.mode = Mode.Desktop
|
||||
when(customerFacade.isEnabled(FeatureType.KeyVerification)).thenResolve(false)
|
||||
|
||||
const isSupported = await keyVerification.isSupported()
|
||||
const isSupported = await withOverriddenEnv({ mode: Mode.Desktop }, () => keyVerification.isSupported())
|
||||
o(isSupported).equals(false)
|
||||
})
|
||||
|
||||
o("feature should NOT be supported when on browser and enabled", async function () {
|
||||
globalThis.env.mode = Mode.Browser
|
||||
when(customerFacade.isEnabled(FeatureType.KeyVerification)).thenResolve(true)
|
||||
|
||||
const isSupported = await keyVerification.isSupported()
|
||||
const isSupported = await withOverriddenEnv({ mode: Mode.Browser }, () => keyVerification.isSupported())
|
||||
o(isSupported).equals(false)
|
||||
})
|
||||
|
||||
|
|
|
@ -36,14 +36,14 @@ import { defer, DeferredObject, uint8ArrayToBase64 } from "@tutao/tutanota-utils
|
|||
import { AccountType, Const, DEFAULT_KDF_TYPE, KdfType } from "../../../../../src/common/api/common/TutanotaConstants"
|
||||
import { AccessExpiredError, ConnectionError, NotAuthenticatedError } from "../../../../../src/common/api/common/error/RestError"
|
||||
import { SessionType } from "../../../../../src/common/api/common/SessionType"
|
||||
import { HttpMethod, resolveClientTypeReference, resolveServerTypeReference } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { HttpMethod, TypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { ConnectMode, EventBusClient } from "../../../../../src/common/api/worker/EventBusClient"
|
||||
import { TutanotaPropertiesTypeRef } from "../../../../../src/common/api/entities/tutanota/TypeRefs"
|
||||
import { BlobAccessTokenFacade } from "../../../../../src/common/api/worker/facades/BlobAccessTokenFacade.js"
|
||||
import { EntropyFacade } from "../../../../../src/common/api/worker/facades/EntropyFacade.js"
|
||||
import { DatabaseKeyFactory } from "../../../../../src/common/misc/credentials/DatabaseKeyFactory.js"
|
||||
import { Argon2idFacade } from "../../../../../src/common/api/worker/facades/Argon2idFacade.js"
|
||||
import { createTestEntity } from "../../../TestUtils.js"
|
||||
import { clientInitializedTypeModelResolver, createTestEntity, instancePipelineFromTypeModelResolver } from "../../../TestUtils.js"
|
||||
import { KeyRotationFacade } from "../../../../../src/common/api/worker/facades/KeyRotationFacade.js"
|
||||
import { CredentialType } from "../../../../../src/common/misc/credentials/CredentialType.js"
|
||||
import { encryptString } from "../../../../../src/common/api/worker/crypto/CryptoWrapper.js"
|
||||
|
@ -120,6 +120,7 @@ o.spec("LoginFacadeTest", function () {
|
|||
let databaseKeyFactoryMock: DatabaseKeyFactory
|
||||
let argon2idFacade: Argon2idFacade
|
||||
let cacheManagmentFacadeMock: CacheManagementFacade
|
||||
let typeModelResolver: TypeModelResolver
|
||||
|
||||
const timeRangeDays = 42
|
||||
const login = "born.slippy@tuta.io"
|
||||
|
@ -135,7 +136,8 @@ o.spec("LoginFacadeTest", function () {
|
|||
when(entityClientMock.loadRoot(TutanotaPropertiesTypeRef, anything())).thenResolve(createTestEntity(TutanotaPropertiesTypeRef))
|
||||
|
||||
loginListener = object<LoginListener>()
|
||||
instancePipeline = new InstancePipeline(resolveClientTypeReference, resolveServerTypeReference)
|
||||
typeModelResolver = clientInitializedTypeModelResolver()
|
||||
instancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
|
||||
cryptoFacadeMock = object<CryptoFacade>()
|
||||
usingOfflineStorage = false
|
||||
cacheStorageInitializerMock = object()
|
||||
|
@ -181,6 +183,7 @@ o.spec("LoginFacadeTest", function () {
|
|||
entityClientMock,
|
||||
async (error: Error) => {},
|
||||
async () => cacheManagmentFacadeMock,
|
||||
typeModelResolver,
|
||||
)
|
||||
|
||||
eventBusClientMock = instance(EventBusClient)
|
||||
|
|
|
@ -40,22 +40,17 @@ import { OfflineStorageMigrator } from "../../../../../src/common/api/worker/off
|
|||
import { InterWindowEventFacadeSendDispatcher } from "../../../../../src/common/native/common/generatedipc/InterWindowEventFacadeSendDispatcher.js"
|
||||
import { untagSqlObject } from "../../../../../src/common/api/worker/offline/SqlValue.js"
|
||||
import { MailSetKind } from "../../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import {
|
||||
globalClientModelInfo,
|
||||
globalServerModelInfo,
|
||||
resolveClientTypeReference,
|
||||
resolveServerTypeReference,
|
||||
} from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { Type as TypeId } from "../../../../../src/common/api/common/EntityConstants.js"
|
||||
import { expandId } from "../../../../../src/common/api/worker/rest/DefaultEntityRestCache.js"
|
||||
import { GroupMembershipTypeRef, UserTypeRef } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { DesktopSqlCipher } from "../../../../../src/common/desktop/db/DesktopSqlCipher.js"
|
||||
import { clientModelAsServerModel, createTestEntity } from "../../../TestUtils.js"
|
||||
import { clientInitializedTypeModelResolver, createTestEntity, modelMapperFromTypeModelResolver } from "../../../TestUtils.js"
|
||||
import { sql } from "../../../../../src/common/api/worker/offline/Sql.js"
|
||||
import { MailOfflineCleaner } from "../../../../../src/mail-app/workerUtils/offline/MailOfflineCleaner.js"
|
||||
import Id from "../../../../../src/mail-app/translations/id.js"
|
||||
import { ModelMapper } from "../../../../../src/common/api/worker/crypto/ModelMapper"
|
||||
import { Entity, ServerModelParsedInstance } from "../../../../../src/common/api/common/EntityTypes"
|
||||
import { TypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
function incrementId(id: Id, ms: number) {
|
||||
const timestamp = generatedIdToTimestamp(id)
|
||||
|
@ -128,6 +123,7 @@ o.spec("OfflineStorageDb", function () {
|
|||
let migratorMock: OfflineStorageMigrator
|
||||
let offlineStorageCleanerMock: OfflineStorageCleaner
|
||||
let interWindowEventSenderMock: InterWindowEventFacadeSendDispatcher
|
||||
let typeModelResolver: TypeModelResolver
|
||||
let modelMapper: ModelMapper
|
||||
|
||||
o.beforeEach(async function () {
|
||||
|
@ -138,10 +134,18 @@ o.spec("OfflineStorageDb", function () {
|
|||
migratorMock = instance(OfflineStorageMigrator)
|
||||
interWindowEventSenderMock = instance(InterWindowEventFacadeSendDispatcher)
|
||||
offlineStorageCleanerMock = new MailOfflineCleaner()
|
||||
modelMapper = new ModelMapper(resolveClientTypeReference, resolveServerTypeReference)
|
||||
clientModelAsServerModel(globalServerModelInfo, globalClientModelInfo)
|
||||
typeModelResolver = clientInitializedTypeModelResolver()
|
||||
modelMapper = modelMapperFromTypeModelResolver(typeModelResolver)
|
||||
when(dateProviderMock.now()).thenReturn(now.getTime())
|
||||
storage = new OfflineStorage(dbFacade, interWindowEventSenderMock, dateProviderMock, migratorMock, offlineStorageCleanerMock, modelMapper)
|
||||
storage = new OfflineStorage(
|
||||
dbFacade,
|
||||
interWindowEventSenderMock,
|
||||
dateProviderMock,
|
||||
migratorMock,
|
||||
offlineStorageCleanerMock,
|
||||
modelMapper,
|
||||
typeModelResolver,
|
||||
)
|
||||
})
|
||||
|
||||
o.afterEach(async function () {
|
||||
|
@ -154,7 +158,7 @@ o.spec("OfflineStorageDb", function () {
|
|||
|
||||
o.spec("Unit test", function () {
|
||||
async function getAllIdsForType(typeRef: TypeRef<unknown>): Promise<Id[]> {
|
||||
const typeModel = await resolveClientTypeReference(typeRef)
|
||||
const typeModel = await typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
let preparedQuery
|
||||
switch (typeModel.type) {
|
||||
case TypeId.Element.valueOf():
|
||||
|
@ -623,7 +627,7 @@ o.spec("OfflineStorageDb", function () {
|
|||
await storage.clearExcludedData(timeRangeDays, userId)
|
||||
|
||||
const newRange = await dbFacade.get("select * from ranges", [])
|
||||
const mailSetEntryTypeModel = await resolveClientTypeReference(MailSetEntryTypeRef)
|
||||
const mailSetEntryTypeModel = await typeModelResolver.resolveClientTypeReference(MailSetEntryTypeRef)
|
||||
o(mapNullable(newRange, untagSqlObject)).deepEquals({
|
||||
type: mailSetEntryType,
|
||||
listId: entriesListId,
|
||||
|
@ -655,7 +659,7 @@ o.spec("OfflineStorageDb", function () {
|
|||
await storage.clearExcludedData(timeRangeDays, userId)
|
||||
|
||||
const newRange = await dbFacade.get("select * from ranges", [])
|
||||
const mailSetEntryTypeModel = await resolveClientTypeReference(MailSetEntryTypeRef)
|
||||
const mailSetEntryTypeModel = await typeModelResolver.resolveClientTypeReference(MailSetEntryTypeRef)
|
||||
o(mapNullable(newRange, untagSqlObject)).deepEquals({
|
||||
type: mailSetEntryType,
|
||||
listId: entriesListId,
|
||||
|
@ -731,7 +735,7 @@ o.spec("OfflineStorageDb", function () {
|
|||
await storage.clearExcludedData(timeRangeDays, userId)
|
||||
|
||||
const newRange = await dbFacade.get("select * from ranges", [])
|
||||
const mailSetEntryTypeModel = await resolveClientTypeReference(MailSetEntryTypeRef)
|
||||
const mailSetEntryTypeModel = await typeModelResolver.resolveClientTypeReference(MailSetEntryTypeRef)
|
||||
o(mapNullable(newRange, untagSqlObject)).deepEquals({
|
||||
type: mailSetEntryType,
|
||||
listId: listIdPart(mailSetEntryId),
|
||||
|
@ -855,7 +859,7 @@ o.spec("OfflineStorageDb", function () {
|
|||
await storage.clearExcludedData(timeRangeDays, userId)
|
||||
|
||||
const newRange = await dbFacade.get("select * from ranges", [])
|
||||
const mailSetEntryTypeModel = await resolveClientTypeReference(MailSetEntryTypeRef)
|
||||
const mailSetEntryTypeModel = await typeModelResolver.resolveClientTypeReference(MailSetEntryTypeRef)
|
||||
o(mapNullable(newRange, untagSqlObject)).deepEquals({
|
||||
type: mailSetEntryType,
|
||||
listId: listIdPart(mailSetEntryId),
|
||||
|
@ -1194,7 +1198,7 @@ o.spec("OfflineStorageDb", function () {
|
|||
|
||||
// Here we clear the excluded data
|
||||
await storage.clearExcludedData(timeRangeDays, userId)
|
||||
const mailSetEntryTypeModel = await resolveClientTypeReference(MailSetEntryTypeRef)
|
||||
const mailSetEntryTypeModel = await typeModelResolver.resolveClientTypeReference(MailSetEntryTypeRef)
|
||||
|
||||
o(await getAllIdsForType(MailFolderTypeRef)).deepEquals([inboxFolderId, spamFolderId, trashFolderId])
|
||||
const allMailSetEntryIds = await getAllIdsForType(MailSetEntryTypeRef)
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import o from "@tutao/otest"
|
||||
import { func, instance, when } from "testdouble"
|
||||
import { func, instance, object, when } from "testdouble"
|
||||
import { verify } from "@tutao/tutanota-test-utils"
|
||||
import { LateInitializedCacheStorageImpl, OfflineStorageArgs } from "../../../../../src/common/api/worker/rest/CacheStorageProxy.js"
|
||||
import { OfflineStorage } from "../../../../../src/common/api/worker/offline/OfflineStorage.js"
|
||||
import { WorkerImpl } from "../../../../../src/mail-app/workerUtils/worker/WorkerImpl.js"
|
||||
import { ModelMapper } from "../../../../../src/common/api/worker/crypto/ModelMapper"
|
||||
import { resolveClientTypeReference } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
o.spec("CacheStorageProxy", function () {
|
||||
const userId = "userId"
|
||||
|
@ -21,12 +19,11 @@ o.spec("CacheStorageProxy", function () {
|
|||
workerMock = instance(WorkerImpl)
|
||||
offlineStorageMock = instance(OfflineStorage)
|
||||
offlineStorageProviderMock = func() as () => Promise<null | OfflineStorage>
|
||||
const modelMapper = new ModelMapper(resolveClientTypeReference, resolveClientTypeReference)
|
||||
proxy = new LateInitializedCacheStorageImpl(
|
||||
modelMapper,
|
||||
async (error: Error) => {
|
||||
await workerMock.sendError(error)
|
||||
},
|
||||
() => Promise.resolve(object()),
|
||||
offlineStorageProviderMock,
|
||||
)
|
||||
})
|
||||
|
@ -35,7 +32,13 @@ o.spec("CacheStorageProxy", function () {
|
|||
o("should create a persistent storage when params are provided and offline storage is enabled", async function () {
|
||||
when(offlineStorageProviderMock()).thenResolve(offlineStorageMock)
|
||||
|
||||
const { isPersistent } = await proxy.initialize({ type: "offline", userId, databaseKey, timeRangeDays: null, forceNewDatabase: false })
|
||||
const { isPersistent } = await proxy.initialize({
|
||||
type: "offline",
|
||||
userId,
|
||||
databaseKey,
|
||||
timeRangeDays: null,
|
||||
forceNewDatabase: false,
|
||||
})
|
||||
|
||||
o(isPersistent).equals(true)
|
||||
})
|
||||
|
@ -51,7 +54,13 @@ o.spec("CacheStorageProxy", function () {
|
|||
o("should create a ephemeral storage when params are provided but offline storage is disabled", async function () {
|
||||
when(offlineStorageProviderMock()).thenResolve(null)
|
||||
|
||||
const { isPersistent } = await proxy.initialize({ type: "offline", userId, databaseKey, timeRangeDays: null, forceNewDatabase: false })
|
||||
const { isPersistent } = await proxy.initialize({
|
||||
type: "offline",
|
||||
userId,
|
||||
databaseKey,
|
||||
timeRangeDays: null,
|
||||
forceNewDatabase: false,
|
||||
})
|
||||
|
||||
o(isPersistent).equals(false)
|
||||
})
|
||||
|
@ -66,7 +75,13 @@ o.spec("CacheStorageProxy", function () {
|
|||
|
||||
o("will flag newDatabase as true when offline storage says it is", async function () {
|
||||
when(offlineStorageProviderMock()).thenResolve(offlineStorageMock)
|
||||
const args: OfflineStorageArgs = { type: "offline", userId, databaseKey, timeRangeDays: null, forceNewDatabase: false }
|
||||
const args: OfflineStorageArgs = {
|
||||
type: "offline",
|
||||
userId,
|
||||
databaseKey,
|
||||
timeRangeDays: null,
|
||||
forceNewDatabase: false,
|
||||
}
|
||||
when(offlineStorageMock.init(args)).thenResolve(true)
|
||||
|
||||
const { isNewOfflineDb } = await proxy.initialize(args)
|
||||
|
@ -76,7 +91,13 @@ o.spec("CacheStorageProxy", function () {
|
|||
|
||||
o("will flag newDatabase as false when offline storage says it is not", async function () {
|
||||
when(offlineStorageProviderMock()).thenResolve(offlineStorageMock)
|
||||
const args: OfflineStorageArgs = { type: "offline", userId, databaseKey, timeRangeDays: null, forceNewDatabase: false }
|
||||
const args: OfflineStorageArgs = {
|
||||
type: "offline",
|
||||
userId,
|
||||
databaseKey,
|
||||
timeRangeDays: null,
|
||||
forceNewDatabase: false,
|
||||
}
|
||||
when(offlineStorageMock.init(args)).thenResolve(false)
|
||||
|
||||
const { isNewOfflineDb } = await proxy.initialize(args)
|
||||
|
@ -89,7 +110,13 @@ o.spec("CacheStorageProxy", function () {
|
|||
|
||||
when(offlineStorageProviderMock()).thenReject(error)
|
||||
|
||||
const { isPersistent } = await proxy.initialize({ type: "offline", userId, databaseKey, timeRangeDays: null, forceNewDatabase: false })
|
||||
const { isPersistent } = await proxy.initialize({
|
||||
type: "offline",
|
||||
userId,
|
||||
databaseKey,
|
||||
timeRangeDays: null,
|
||||
forceNewDatabase: false,
|
||||
})
|
||||
|
||||
o(isPersistent).equals(false)
|
||||
verify(workerMock.sendError(error))
|
||||
|
|
|
@ -7,22 +7,15 @@ import { EntityRestClient } from "../../../../../src/common/api/worker/rest/Enti
|
|||
import { LateInitializedCacheStorageImpl } from "../../../../../src/common/api/worker/rest/CacheStorageProxy.js"
|
||||
import { CUSTOM_MAX_ID, CUSTOM_MIN_ID, LOAD_MULTIPLE_LIMIT } from "../../../../../src/common/api/common/utils/EntityUtils.js"
|
||||
import { numberRange, promiseMap } from "@tutao/tutanota-utils"
|
||||
import { clientModelAsServerModel, createTestEntity } from "../../../TestUtils.js"
|
||||
import { clientInitializedTypeModelResolver, clientModelAsServerModel, createTestEntity, modelMapperFromTypeModelResolver } from "../../../TestUtils.js"
|
||||
import { ModelMapper } from "../../../../../src/common/api/worker/crypto/ModelMapper"
|
||||
import {
|
||||
globalClientModelInfo,
|
||||
globalServerModelInfo,
|
||||
resolveClientTypeReference,
|
||||
resolveServerTypeReference,
|
||||
} from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { ServerModelParsedInstance } from "../../../../../src/common/api/common/EntityTypes"
|
||||
|
||||
o.spec("Custom calendar events handler", function () {
|
||||
const entityRestClientMock = instance(EntityRestClient)
|
||||
const cacheHandler = new CustomCalendarEventCacheHandler(entityRestClientMock)
|
||||
let cacheHandler: CustomCalendarEventCacheHandler
|
||||
const offlineStorageMock = instance(LateInitializedCacheStorageImpl)
|
||||
const modelMapper = new ModelMapper(resolveClientTypeReference, resolveServerTypeReference)
|
||||
clientModelAsServerModel(globalServerModelInfo, globalClientModelInfo)
|
||||
let modelMapper: ModelMapper
|
||||
const listId = "listId"
|
||||
let timestamp = Date.now()
|
||||
const ids = [0, 1, 2, 3, 4, 5, 6].map((n) => createEventElementId(timestamp, n))
|
||||
|
@ -33,6 +26,12 @@ o.spec("Custom calendar events handler", function () {
|
|||
const bigList = numberRange(0, 299).map((n) => createTestEntity(CalendarEventTypeRef, { _id: [bigListId, bigListIds[n]] }))
|
||||
const toElementId = (e) => e._id[1]
|
||||
|
||||
o.beforeEach(() => {
|
||||
const typeModelResolver = clientInitializedTypeModelResolver()
|
||||
modelMapper = modelMapperFromTypeModelResolver(typeModelResolver)
|
||||
cacheHandler = new CustomCalendarEventCacheHandler(entityRestClientMock, typeModelResolver)
|
||||
})
|
||||
|
||||
o.spec("Load elements from cache", function () {
|
||||
o.beforeEach(async function () {
|
||||
const allListParsedInstance = await promiseMap(
|
||||
|
|
|
@ -17,8 +17,6 @@ import {
|
|||
import { arrayOf, clone, deepEqual, downcast, isSameTypeRef, last, promiseMap, TypeRef } from "@tutao/tutanota-utils"
|
||||
import {
|
||||
CustomerTypeRef,
|
||||
EntityUpdate,
|
||||
EntityUpdateTypeRef,
|
||||
ExternalUserReferenceTypeRef,
|
||||
GroupKeyTypeRef,
|
||||
GroupMembershipTypeRef,
|
||||
|
@ -28,8 +26,6 @@ import {
|
|||
RootInstanceTypeRef,
|
||||
UserTypeRef,
|
||||
} from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { CacheMode, EntityRestClient, typeRefToRestPath } from "../../../../../src/common/api/worker/rest/EntityRestClient.js"
|
||||
import { QueuedBatch } from "../../../../../src/common/api/worker/EventQueue.js"
|
||||
import { CacheStorage, DefaultEntityRestCache, EXTEND_RANGE_MIN_CHUNK_SIZE } from "../../../../../src/common/api/worker/rest/DefaultEntityRestCache.js"
|
||||
import {
|
||||
BodyTypeRef,
|
||||
|
@ -57,10 +53,12 @@ import { createEventElementId } from "../../../../../src/common/api/common/utils
|
|||
import { InterWindowEventFacadeSendDispatcher } from "../../../../../src/common/native/common/generatedipc/InterWindowEventFacadeSendDispatcher.js"
|
||||
import { func, instance, matchers, object, replace, when } from "testdouble"
|
||||
import { SqlCipherFacade } from "../../../../../src/common/native/common/generatedipc/SqlCipherFacade.js"
|
||||
import { clientModelAsServerModel, createTestEntity } from "../../../TestUtils.js"
|
||||
import { clientInitializedTypeModelResolver, createTestEntity, modelMapperFromTypeModelResolver } from "../../../TestUtils.js"
|
||||
import { ModelMapper } from "../../../../../src/common/api/worker/crypto/ModelMapper"
|
||||
import { globalClientModelInfo, globalServerModelInfo, resolveClientTypeReference } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { Entity, ServerModelParsedInstance } from "../../../../../src/common/api/common/EntityTypes"
|
||||
import { CacheMode, EntityRestClient } from "../../../../../src/common/api/worker/rest/EntityRestClient.js"
|
||||
import { TypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { EntityUpdateData } from "../../../../../src/common/api/common/utils/EntityUpdateUtils"
|
||||
|
||||
const { anything } = matchers
|
||||
|
||||
|
@ -87,6 +85,7 @@ async function getOfflineStorage(userId: Id): Promise<CacheStorage> {
|
|||
await sqlCipherFacade.openDb(userId, offlineDatabaseTestKey)
|
||||
const interWindowEventSender = instance(InterWindowEventFacadeSendDispatcher)
|
||||
const offlineStorageCleanerMock = object<OfflineStorageCleaner>()
|
||||
const typeModelResolver = clientInitializedTypeModelResolver()
|
||||
|
||||
const offlineStorage = new OfflineStorage(
|
||||
sqlCipherFacade,
|
||||
|
@ -94,48 +93,49 @@ async function getOfflineStorage(userId: Id): Promise<CacheStorage> {
|
|||
new NoZoneDateProvider(),
|
||||
migratorMock,
|
||||
offlineStorageCleanerMock,
|
||||
new ModelMapper(resolveClientTypeReference, resolveClientTypeReference),
|
||||
modelMapperFromTypeModelResolver(typeModelResolver),
|
||||
typeModelResolver,
|
||||
)
|
||||
await offlineStorage.init({ userId, databaseKey: offlineDatabaseTestKey, timeRangeDays: 42, forceNewDatabase: false })
|
||||
return offlineStorage
|
||||
}
|
||||
|
||||
async function getEphemeralStorage(): Promise<EphemeralCacheStorage> {
|
||||
const modelMapper = new ModelMapper(resolveClientTypeReference, resolveClientTypeReference)
|
||||
return new EphemeralCacheStorage(modelMapper)
|
||||
const typeModelResolver = clientInitializedTypeModelResolver()
|
||||
const modelMapper = modelMapperFromTypeModelResolver(typeModelResolver)
|
||||
return new EphemeralCacheStorage(modelMapper, typeModelResolver)
|
||||
}
|
||||
|
||||
testEntityRestCache("ephemeral", getEphemeralStorage)
|
||||
|
||||
node(() => testEntityRestCache("offline", getOfflineStorage))()
|
||||
|
||||
async function toStorableInstance(entity: Entity): Promise<ServerModelParsedInstance> {
|
||||
return downcast<ServerModelParsedInstance>(
|
||||
await new ModelMapper(resolveClientTypeReference, resolveClientTypeReference).mapToClientModelParsedInstance(entity._type, entity),
|
||||
)
|
||||
}
|
||||
|
||||
export function testEntityRestCache(name: string, getStorage: (userId: Id) => Promise<CacheStorage>) {
|
||||
const groupId = "groupId"
|
||||
const batchId = "batchId"
|
||||
|
||||
o.spec(`EntityRestCache ${name}`, function () {
|
||||
let storage: CacheStorage
|
||||
let cache: DefaultEntityRestCache
|
||||
let typeModelResolver: TypeModelResolver
|
||||
let modelMapper: ModelMapper
|
||||
|
||||
// The entity client will assert to throwing if an unexpected method is called
|
||||
// You can mock it's attributes if you want to assert that a given method will be called
|
||||
let entityRestClient: EntityRestClient
|
||||
let userId: Id | null
|
||||
|
||||
let createUpdate = function (typeRef: TypeRef<any>, listId: Id, id: Id, operation: OperationType): EntityUpdate {
|
||||
return createTestEntity(EntityUpdateTypeRef, {
|
||||
application: typeRef.app,
|
||||
type: typeRef.typeId.toString(),
|
||||
typeId: typeRef.typeId.toString(),
|
||||
instanceListId: listId,
|
||||
async function toStorableInstance(entity: Entity): Promise<ServerModelParsedInstance> {
|
||||
return downcast<ServerModelParsedInstance>(await modelMapper.mapToClientModelParsedInstance(entity._type, entity))
|
||||
}
|
||||
|
||||
let createUpdate = function (typeRef: TypeRef<any>, listId: Id, id: Id, operation: OperationType): EntityUpdateData {
|
||||
return {
|
||||
typeRef: typeRef,
|
||||
instanceId: id,
|
||||
operation: operation,
|
||||
})
|
||||
instanceListId: listId,
|
||||
operation,
|
||||
}
|
||||
}
|
||||
|
||||
let createId = function (idText) {
|
||||
|
@ -173,7 +173,6 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
}
|
||||
|
||||
function mockRestClient(): EntityRestClient {
|
||||
const modelMapper = new ModelMapper(resolveClientTypeReference, resolveClientTypeReference)
|
||||
const restClient = object<RestClient>()
|
||||
const entityRestClient = object<EntityRestClient>()
|
||||
|
||||
|
@ -187,7 +186,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
when(entityRestClient.mapInstancesToEntity(typeRefCaptor.capture(), serverModelParsedInstanceCaptor.capture())).thenDo(async () =>
|
||||
downcast<any>(await modelMapper.mapToInstances(typeRefCaptor.value, serverModelParsedInstanceCaptor.value)),
|
||||
)
|
||||
when(entityRestClient.entityEventsReceived(batchCaptor.capture())).thenResolve(batchCaptor.value)
|
||||
when(entityRestClient.entityEventsReceived(batchCaptor.capture(), matchers.anything(), matchers.anything())).thenResolve(batchCaptor.value)
|
||||
when(entityRestClient.getRestClient()).thenReturn(restClient)
|
||||
return entityRestClient
|
||||
}
|
||||
|
@ -195,12 +194,13 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
o.beforeEach(async function () {
|
||||
userId = "userId"
|
||||
storage = await getStorage(userId)
|
||||
typeModelResolver = clientInitializedTypeModelResolver()
|
||||
modelMapper = modelMapperFromTypeModelResolver(typeModelResolver)
|
||||
entityRestClient = mockRestClient()
|
||||
cache = new DefaultEntityRestCache(entityRestClient, storage)
|
||||
cache = new DefaultEntityRestCache(entityRestClient, storage, typeModelResolver)
|
||||
})
|
||||
|
||||
o.spec("entityEventsReceived", function () {
|
||||
const path = typeRefToRestPath(ContactTypeRef)
|
||||
const contactListId1 = "contactListId1"
|
||||
const contactListId2 = "contactListId2"
|
||||
const id1 = "id1"
|
||||
|
@ -233,14 +233,13 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
when(putLastBatchIdForGroup(groupId, batchId)).thenResolve(undefined)
|
||||
replace(storage, "putLastBatchIdForGroup", putLastBatchIdForGroup)
|
||||
|
||||
await cache.entityEventsReceived(makeBatch(batch))
|
||||
await cache.entityEventsReceived(batch, "batchId", groupId)
|
||||
await cache.getLastEntityEventBatchForGroup(groupId)
|
||||
verify(putLastBatchIdForGroup(groupId, batchId))
|
||||
})
|
||||
|
||||
o.spec("postMultiple", function () {
|
||||
o.beforeEach(async function () {
|
||||
clientModelAsServerModel(globalServerModelInfo, globalClientModelInfo)
|
||||
await storage.setNewRangeForList(ContactTypeRef, contactListId1, id1, id7)
|
||||
await storage.setNewRangeForList(ContactTypeRef, contactListId2, id1, id7)
|
||||
//when using offline calendar ids are always in cache range
|
||||
|
@ -264,7 +263,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
when(entityRestClient.loadMultipleParsedInstances(ContactTypeRef, contactListId1, ["id1", "id2"], anything(), anything())).thenResolve(
|
||||
await promiseMap([contact1, contact2], (contact) => toStorableInstance(contact)),
|
||||
)
|
||||
const updates = await cache.entityEventsReceived(makeBatch(batch))
|
||||
const updates = await cache.entityEventsReceived(batch, "batchId", groupId)
|
||||
verify(entityRestClient.loadMultipleParsedInstances(ContactTypeRef, contactListId1, ["id1", "id2"], anything(), anything()), { times: 1 })
|
||||
o(await storage.get(ContactTypeRef, contactListId1, id1)).notEquals(null)
|
||||
o(await storage.get(ContactTypeRef, contactListId1, id2)).notEquals(null)
|
||||
|
@ -301,7 +300,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
anything(),
|
||||
),
|
||||
).thenResolve(await promiseMap([event1, event2], (contact) => toStorableInstance(contact)))
|
||||
const updates = await cache.entityEventsReceived(makeBatch(batch))
|
||||
const updates = await cache.entityEventsReceived(batch, "batchId", groupId)
|
||||
verify(
|
||||
entityRestClient.loadMultipleParsedInstances(
|
||||
CalendarEventTypeRef,
|
||||
|
@ -409,7 +408,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
if (name === "offline") {
|
||||
await storage.setNewRangeForList(CalendarEventTypeRef, calendarEventListId, CUSTOM_MIN_ID, CUSTOM_MAX_ID)
|
||||
}
|
||||
const filteredUpdates = await cache.entityEventsReceived(makeBatch(batch))
|
||||
const filteredUpdates = await cache.entityEventsReceived(batch, "batchId", groupId)
|
||||
verify(entityRestClient.loadParsedInstance(ContactTypeRef, ["contactListId1", "id2"]), { times: 1 }) // One load for contact update
|
||||
o(await storage.get(ContactTypeRef, contactListId1, id1)).notEquals(null)
|
||||
o(await storage.get(ContactTypeRef, contactListId1, id2)).notEquals(null)
|
||||
|
@ -466,7 +465,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
new NotAuthorizedError("bam"),
|
||||
)
|
||||
|
||||
const updates = await cache.entityEventsReceived(makeBatch(batch))
|
||||
const updates = await cache.entityEventsReceived(batch, "batchId", groupId)
|
||||
|
||||
verify(entityRestClient.loadMultipleParsedInstances(ContactTypeRef, anything(), anything(), anything(), anything()), { times: 2 })
|
||||
o(await storage.get(ContactTypeRef, contactListId1, id1)).notEquals(null)
|
||||
|
@ -482,7 +481,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
createUpdate(ContactTypeRef, contactListId1, id1, OperationType.CREATE),
|
||||
createUpdate(ContactTypeRef, contactListId1, id2, OperationType.CREATE),
|
||||
]
|
||||
const updates = await cache.entityEventsReceived(makeBatch(batch))
|
||||
const updates = await cache.entityEventsReceived(batch, "batchId", groupId)
|
||||
|
||||
o(await storage.get(ContactTypeRef, contactListId1, id1)).equals(null)
|
||||
o(await storage.get(ContactTypeRef, contactListId1, id2)).equals(null)
|
||||
|
@ -509,7 +508,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
await promiseMap(contacts, (contact) => toStorableInstance(contact)),
|
||||
)
|
||||
|
||||
const filteredUpdates = await cache.entityEventsReceived(makeBatch(batch))
|
||||
const filteredUpdates = await cache.entityEventsReceived(batch, "batchId", groupId)
|
||||
verify(entityRestClient.loadMultipleParsedInstances(ContactTypeRef, contactListId1, ["id1", "id2"], anything(), anything()), { times: 1 })
|
||||
o(await storage.get(ContactTypeRef, contactListId1, id1)).notEquals(null)
|
||||
o(await storage.get(ContactTypeRef, contactListId1, id2)).equals(null)
|
||||
|
@ -553,7 +552,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
await promiseMap(secondContactsList, (contact) => toStorableInstance(contact)),
|
||||
)
|
||||
|
||||
const filteredUpdates = await cache.entityEventsReceived(makeBatch(batch))
|
||||
const filteredUpdates = await cache.entityEventsReceived(batch, "batchId", groupId)
|
||||
verify(entityRestClient.loadMultipleParsedInstances(ContactTypeRef, anything(), anything(), anything(), anything()), { times: 2 })
|
||||
o(await storage.get(ContactTypeRef, contactListId1, id1)).notEquals(null)
|
||||
o(await storage.get(ContactTypeRef, contactListId1, id2)).equals(null)
|
||||
|
@ -591,7 +590,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
new NotAuthorizedError("bam"),
|
||||
)
|
||||
|
||||
const filteredUpdates = await cache.entityEventsReceived(makeBatch(batch))
|
||||
const filteredUpdates = await cache.entityEventsReceived(batch, "batchId", groupId)
|
||||
|
||||
verify(entityRestClient.loadMultipleParsedInstances(ContactTypeRef, anything(), anything(), anything(), anything()), { times: 2 })
|
||||
o(await storage.get(ContactTypeRef, contactListId1, id1)).notEquals(null)
|
||||
|
@ -605,11 +604,11 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
})
|
||||
})
|
||||
o("element create notifications are not loaded from server", async function () {
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(MailBoxTypeRef, null as any, "id1", OperationType.CREATE)]))
|
||||
await cache.entityEventsReceived([createUpdate(MailBoxTypeRef, null as any, "id1", OperationType.CREATE)], "batchId", groupId)
|
||||
})
|
||||
|
||||
o("element update notifications are not put into cache", async function () {
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(MailBoxTypeRef, null as any, "id1", OperationType.UPDATE)]))
|
||||
await cache.entityEventsReceived([createUpdate(MailBoxTypeRef, null as any, "id1", OperationType.UPDATE)], "batchId", groupId)
|
||||
})
|
||||
|
||||
// element notifications
|
||||
|
@ -622,7 +621,11 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
when(entityRestClient.loadParsedInstance(MailDetailsBlobTypeRef, [archiveId, createId(mailDetailsId)])).thenResolve(
|
||||
await toStorableInstance(createMailDetailsBlobInstance(archiveId, createId(mailDetailsId), "goodbye")),
|
||||
)
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(MailDetailsBlobTypeRef, archiveId, createId(mailDetailsId), OperationType.UPDATE)]))
|
||||
await cache.entityEventsReceived(
|
||||
[createUpdate(MailDetailsBlobTypeRef, archiveId, createId(mailDetailsId), OperationType.UPDATE)],
|
||||
"batchId",
|
||||
groupId,
|
||||
)
|
||||
verify(entityRestClient.loadParsedInstance(MailDetailsBlobTypeRef, [archiveId, createId(mailDetailsId)]), { times: 1 })
|
||||
const blob = await cache.load(MailDetailsBlobTypeRef, [archiveId, createId(mailDetailsId)])
|
||||
o(blob.details.body.text).equals("goodbye")
|
||||
|
@ -637,7 +640,11 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
await storage.put(MailDetailsBlobTypeRef, await toStorableInstance(mailDetailsBlob))
|
||||
|
||||
when(entityRestClient.loadParsedInstance(MailDetailsBlobTypeRef, [archiveId, createId(mailDetailsId)])).thenReject(new NotFoundError("test!"))
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(MailDetailsBlobTypeRef, archiveId, createId(mailDetailsId), OperationType.UPDATE)]))
|
||||
await cache.entityEventsReceived(
|
||||
[createUpdate(MailDetailsBlobTypeRef, archiveId, createId(mailDetailsId), OperationType.UPDATE)],
|
||||
"batchId",
|
||||
groupId,
|
||||
)
|
||||
verify(entityRestClient.loadParsedInstance(MailDetailsBlobTypeRef, [archiveId, createId(mailDetailsId)]), { times: 1 })
|
||||
o(await storage.get(mailDetailsBlob._type, archiveId, createId(mailDetailsId))).equals(null)("the blob is deleted from the cache")
|
||||
})
|
||||
|
@ -657,10 +664,12 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
|
||||
// The moved mail will not be loaded from the server
|
||||
await cache.entityEventsReceived(
|
||||
makeBatch([
|
||||
[
|
||||
createUpdate(MailSetEntryTypeRef, getListId(instance), getElementId(instance), OperationType.DELETE),
|
||||
createUpdate(MailSetEntryTypeRef, newListId, getElementId(instance), OperationType.CREATE),
|
||||
]),
|
||||
],
|
||||
"batchId",
|
||||
groupId,
|
||||
)
|
||||
|
||||
when(entityRestClient.loadParsedInstance(MailSetEntryTypeRef, instance._id, anything())).thenReject(new NotFoundError("error from test"))
|
||||
|
@ -678,7 +687,11 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
await storage.put(MailDetailsBlobTypeRef, await toStorableInstance(mailDetailsBlob))
|
||||
|
||||
when(entityRestClient.loadParsedInstance(MailDetailsBlobTypeRef, mailDetailsBlob._id, anything())).thenReject(new NotFoundError("not found"))
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(MailDetailsBlobTypeRef, archiveId, createId(mailDetailsId), OperationType.DELETE)]))
|
||||
await cache.entityEventsReceived(
|
||||
[createUpdate(MailDetailsBlobTypeRef, archiveId, createId(mailDetailsId), OperationType.DELETE)],
|
||||
"batchId",
|
||||
groupId,
|
||||
)
|
||||
// entity is not loaded from server when it is deleted
|
||||
verify(entityRestClient.loadParsedInstance(MailDetailsBlobTypeRef, mailDetailsBlob._id, anything()), { times: 0 })
|
||||
await assertThrows(NotFoundError, () => cache.load(MailDetailsBlobTypeRef, [archiveId, createId(mailDetailsId)]))
|
||||
|
@ -708,10 +721,12 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
verify(entityRestClient.loadParsedInstancesRange(MailSetEntryTypeRef, listId, GENERATED_MIN_ID, 3, false, anything()), { times: 1 })
|
||||
// Move mail event: we don't try to load the mail again, we just update our cached mail
|
||||
await cache.entityEventsReceived(
|
||||
makeBatch([
|
||||
[
|
||||
createUpdate(MailSetEntryTypeRef, getListId(mailSetEntries[0]), getElementId(mailSetEntries[0]), OperationType.DELETE),
|
||||
createUpdate(MailSetEntryTypeRef, newListId, getElementId(mailSetEntries[0]), OperationType.CREATE),
|
||||
]),
|
||||
],
|
||||
"batchId",
|
||||
groupId,
|
||||
)
|
||||
|
||||
// id1 was moved to another list, which means it is no longer cached, which means we should try to load it again (causing NotFoundError)
|
||||
|
@ -742,10 +757,12 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
|
||||
// Move mail event: we don't try to load the mail again, we just update our cached mail
|
||||
await cache.entityEventsReceived(
|
||||
makeBatch([
|
||||
[
|
||||
createUpdate(MailSetEntryTypeRef, "listId1", getElementId(mailSetEntries[2]), OperationType.DELETE),
|
||||
createUpdate(MailSetEntryTypeRef, "listId2", getElementId(mailSetEntries[2]), OperationType.CREATE),
|
||||
]),
|
||||
],
|
||||
"batchId",
|
||||
groupId,
|
||||
)
|
||||
|
||||
// id3 was moved to another list, which means it is no longer cached, which means we should try to load it again when requested (causing NotFoundError)
|
||||
|
@ -766,7 +783,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
await storage.put(MailTypeRef, await toStorableInstance(mail))
|
||||
await storage.put(MailDetailsBlobTypeRef, await toStorableInstance(mailDetailsBlob))
|
||||
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(MailTypeRef, getListId(mail), getElementId(mail), OperationType.DELETE)]))
|
||||
await cache.entityEventsReceived([createUpdate(MailTypeRef, getListId(mail), getElementId(mail), OperationType.DELETE)], "batchId", groupId)
|
||||
|
||||
o(await storage.get(MailTypeRef, getListId(mail), getElementId(mail))).equals(null)
|
||||
o(await storage.get(MailDetailsBlobTypeRef, getListId(mailDetailsBlob), getElementId(mailDetailsBlob))).equals(null)
|
||||
|
@ -779,19 +796,19 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
const mail = createMailInstance("listId1", "id1", "i am a mail")
|
||||
when(entityRestClient.loadParsedInstance(MailTypeRef, mail._id)).thenResolve(await toStorableInstance(mail))
|
||||
when(entityRestClient.load(MailTypeRef, mail._id)).thenResolve(mail)
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(MailTypeRef, getListId(mail), getElementId(mail), OperationType.CREATE)]))
|
||||
await cache.entityEventsReceived([createUpdate(MailTypeRef, getListId(mail), getElementId(mail), OperationType.CREATE)], "batchId", groupId)
|
||||
|
||||
o(await storage.get(MailTypeRef, getListId(mail), getElementId(mail))).deepEquals(mail)
|
||||
})
|
||||
} else {
|
||||
// With ephemeral cache we do not automatically download all mails because we don't need to.
|
||||
o("when the list is not cached, mail create notifications are not put into cache", async function () {
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(MailTypeRef, "listId1", createId("id1"), OperationType.CREATE)]))
|
||||
await cache.entityEventsReceived([createUpdate(MailTypeRef, "listId1", createId("id1"), OperationType.CREATE)], "batchId", groupId)
|
||||
})
|
||||
}
|
||||
|
||||
o("list element update notifications are not put into cache", async function () {
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(MailTypeRef, "listId1", createId("id1"), OperationType.UPDATE)]))
|
||||
await cache.entityEventsReceived([createUpdate(MailTypeRef, "listId1", createId("id1"), OperationType.UPDATE)], "batchId", groupId)
|
||||
})
|
||||
|
||||
o("list element is updated in cache", async function () {
|
||||
|
@ -802,7 +819,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
|
||||
when(entityRestClient.loadParsedInstance(MailTypeRef, initialMail._id)).thenResolve(await toStorableInstance(mailUpdate))
|
||||
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(MailTypeRef, "listId1", createId("id1"), OperationType.UPDATE)]))
|
||||
await cache.entityEventsReceived([createUpdate(MailTypeRef, "listId1", createId("id1"), OperationType.UPDATE)], "batchId", groupId)
|
||||
verify(entityRestClient.loadParsedInstance(MailTypeRef, initialMail._id), { times: 1 })
|
||||
|
||||
const mail = await cache.load(MailTypeRef, ["listId1", createId("id1")])
|
||||
|
@ -813,7 +830,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
o("when deleted from a range, then the remaining range will still be retrieved from the cache", async function () {
|
||||
const originalMails = await setupMailList(true, true)
|
||||
// no load should be called
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(MailTypeRef, "listId1", createId("id2"), OperationType.DELETE)]))
|
||||
await cache.entityEventsReceived([createUpdate(MailTypeRef, "listId1", createId("id2"), OperationType.DELETE)], "batchId", groupId)
|
||||
const mails = await cache.loadRange(MailTypeRef, "listId1", GENERATED_MIN_ID, 4, false)
|
||||
// The entity is provided from the cache
|
||||
o(mails).deepEquals([originalMails[0], originalMails[2]])
|
||||
|
@ -871,7 +888,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
await storage.putLastBatchIdForGroup(calendarGroupId, "1")
|
||||
storage.getUserId = () => userId
|
||||
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(UserTypeRef, "", userId, OperationType.UPDATE)]))
|
||||
await cache.entityEventsReceived([createUpdate(UserTypeRef, "", userId, OperationType.UPDATE)], "batchId", groupId)
|
||||
|
||||
o(await storage.get(CalendarEventTypeRef, listIdPart(eventId), elementIdPart(eventId))).notEquals(null)("Event has been evicted from cache")
|
||||
o(await storage.getLastBatchIdForGroup(calendarGroupId)).notEquals(null)
|
||||
|
@ -952,7 +969,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
await storage.putLastBatchIdForGroup(calendarGroupId, "1")
|
||||
storage.getUserId = () => userId
|
||||
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(UserTypeRef, "", userId, OperationType.UPDATE)]))
|
||||
await cache.entityEventsReceived([createUpdate(UserTypeRef, "", userId, OperationType.UPDATE)], "batchId", groupId)
|
||||
|
||||
o(await storage.get(CalendarEventTypeRef, null, groupRootId)).equals(null)("GroupRoot has been evicted from cache")
|
||||
o(await storage.getLastBatchIdForGroup(calendarGroupId)).equals(null)
|
||||
|
@ -1033,7 +1050,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
await storage.putLastBatchIdForGroup?.(calendarGroupId, "1")
|
||||
storage.getUserId = () => userId
|
||||
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(UserTypeRef, "", userId, OperationType.UPDATE)]))
|
||||
await cache.entityEventsReceived([createUpdate(UserTypeRef, "", userId, OperationType.UPDATE)], "batchId", groupId)
|
||||
|
||||
o(await storage.get(CalendarEventTypeRef, listIdPart(eventId), elementIdPart(eventId))).equals(null)("Event has been evicted from cache")
|
||||
const deletedRange = await storage.getRangeForList(CalendarEventTypeRef, listIdPart(eventId))
|
||||
|
@ -1116,7 +1133,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
await storage.put(CalendarEventTypeRef, await toStorableInstance(event))
|
||||
storage.getUserId = () => "anotherUserId"
|
||||
|
||||
await cache.entityEventsReceived(makeBatch([createUpdate(UserTypeRef, "", userId, OperationType.UPDATE)]))
|
||||
await cache.entityEventsReceived([createUpdate(UserTypeRef, "", userId, OperationType.UPDATE)], "batchId", groupId)
|
||||
|
||||
o(await storage.get(CalendarEventTypeRef, listIdPart(eventId), elementIdPart(eventId))).notEquals(null)("Event has been evicted from cache")
|
||||
})
|
||||
|
@ -1553,7 +1570,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
"When there is a non-reverse range request that loads away from the existing range, the range will grow to include startId + the rest from the server",
|
||||
async function () {
|
||||
const clientMock = object<EntityRestClient>()
|
||||
const cache = new DefaultEntityRestCache(clientMock, storage)
|
||||
const cache = new DefaultEntityRestCache(clientMock, storage, typeModelResolver)
|
||||
|
||||
const listId = "listId"
|
||||
|
||||
|
@ -1599,7 +1616,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
"When there is a non-reverse range request that loads in the direction of the existing range, the range will grow to include the startId",
|
||||
async function () {
|
||||
const clientMock = object<EntityRestClient>()
|
||||
const cache = new DefaultEntityRestCache(clientMock, storage)
|
||||
const cache = new DefaultEntityRestCache(clientMock, storage, typeModelResolver)
|
||||
|
||||
const listId = "listId1"
|
||||
|
||||
|
@ -1652,7 +1669,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
"When there is a reverse range request that loads in the direction of the existing range, the range will grow to include the startId",
|
||||
async function () {
|
||||
const clientMock = object<EntityRestClient>()
|
||||
const cache = new DefaultEntityRestCache(clientMock, storage)
|
||||
const cache = new DefaultEntityRestCache(clientMock, storage, typeModelResolver)
|
||||
|
||||
const listId = "listId1"
|
||||
const mails = arrayOf(100, (idx) => createMailInstance(listId, createId(`${idx}`), `hola ${idx}`))
|
||||
|
@ -1689,7 +1706,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
"The range request starts on one end of the existing range, and would finish on the other end, so it loads from either direction of the range",
|
||||
async function () {
|
||||
const clientMock = object<EntityRestClient>()
|
||||
const cache = new DefaultEntityRestCache(clientMock, storage)
|
||||
const cache = new DefaultEntityRestCache(clientMock, storage, typeModelResolver)
|
||||
|
||||
const id1 = createId("1")
|
||||
const id2 = createId("2")
|
||||
|
@ -1818,7 +1835,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
})
|
||||
return toStorableInstance(contact)
|
||||
})
|
||||
const cache = new DefaultEntityRestCache(entityRestClient, storage)
|
||||
const cache = new DefaultEntityRestCache(entityRestClient, storage, typeModelResolver)
|
||||
await cache.load(ContactTypeRef, contactId, {
|
||||
queryParams: {
|
||||
myParam: "param",
|
||||
|
@ -1837,7 +1854,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
_ownerGroup: "owner-group",
|
||||
})
|
||||
when(entityRestClient.loadParsedInstance(anything(), anything(), anything())).thenResolve(await toStorableInstance(contactOnTheServer))
|
||||
const cache = new DefaultEntityRestCache(entityRestClient, storage)
|
||||
const cache = new DefaultEntityRestCache(entityRestClient, storage, typeModelResolver)
|
||||
const firstLoaded = await cache.load(ContactTypeRef, contactId)
|
||||
o(firstLoaded).deepEquals(contactOnTheServer)
|
||||
// @ts-ignore
|
||||
|
@ -1885,7 +1902,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
return permissionOnTheServer
|
||||
}),
|
||||
})
|
||||
const cache = new DefaultEntityRestCache(client, storage)
|
||||
const cache = new DefaultEntityRestCache(client, storage, typeModelResolver)
|
||||
await cache.load(PermissionTypeRef, permissionId)
|
||||
await cache.load(PermissionTypeRef, permissionId)
|
||||
// @ts-ignore
|
||||
|
@ -1901,7 +1918,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
_ownerGroup: "owner-group",
|
||||
})
|
||||
when(client.loadParsedInstance(MailAddressToGroupTypeRef, id, anything())).thenResolve(await toStorableInstance(entity))
|
||||
const cache = new DefaultEntityRestCache(client, storage)
|
||||
const cache = new DefaultEntityRestCache(client, storage, typeModelResolver)
|
||||
|
||||
const loadedEntity = await cache.load(MailAddressToGroupTypeRef, id)
|
||||
await cache.load(MailAddressToGroupTypeRef, id)
|
||||
|
@ -1920,7 +1937,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
_ownerGroup: "owner-group",
|
||||
})
|
||||
when(client.loadParsedInstance(RootInstanceTypeRef, id, anything())).thenResolve(await toStorableInstance(entity))
|
||||
const cache = new DefaultEntityRestCache(client, storage)
|
||||
const cache = new DefaultEntityRestCache(client, storage, typeModelResolver)
|
||||
|
||||
const loadedEntity = await cache.load(RootInstanceTypeRef, id)
|
||||
await cache.load(RootInstanceTypeRef, id)
|
||||
|
@ -1947,7 +1964,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
await toStorableInstance(firstEntity),
|
||||
await toStorableInstance(secondEntity),
|
||||
])
|
||||
const cache = new DefaultEntityRestCache(client, storage)
|
||||
const cache = new DefaultEntityRestCache(client, storage, typeModelResolver)
|
||||
|
||||
const loadedEntity = await cache.loadMultiple(MailAddressToGroupTypeRef, null, ids)
|
||||
await cache.loadMultiple(MailAddressToGroupTypeRef, null, ids)
|
||||
|
@ -1982,7 +1999,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
when(client.loadMultipleParsedInstances(RootInstanceTypeRef, listId, [elementIdPart(ids[0]), elementIdPart(ids[1])], anything()), {
|
||||
ignoreExtraArgs: true,
|
||||
}).thenResolve([await toStorableInstance(firstEntity), await toStorableInstance(secondEntity)])
|
||||
const cache = new DefaultEntityRestCache(client, storage)
|
||||
const cache = new DefaultEntityRestCache(client, storage, typeModelResolver)
|
||||
|
||||
const loadedEntity = await cache.loadMultiple(RootInstanceTypeRef, listId, [elementIdPart(ids[0]), elementIdPart(ids[1])])
|
||||
await cache.loadMultiple(RootInstanceTypeRef, listId, [elementIdPart(ids[0]), elementIdPart(ids[1])])
|
||||
|
@ -2033,7 +2050,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
|
||||
const client: EntityRestClient = mockRestClient()
|
||||
when(client.loadParsedInstance(ContactTypeRef, contactId, anything())).thenResolve(await toStorableInstance(contactOnTheServer))
|
||||
const cache = new DefaultEntityRestCache(client, storage)
|
||||
const cache = new DefaultEntityRestCache(client, storage, typeModelResolver)
|
||||
|
||||
const cacheBypassed1 = await cache.load(ContactTypeRef, contactId, { cacheMode: CacheMode.WriteOnly })
|
||||
o(cacheBypassed1).deepEquals(contactOnTheServer)
|
||||
|
@ -2083,7 +2100,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
when(
|
||||
client.loadMultipleParsedInstances(ContactTypeRef, listId, [elementIdPart(contactAId), elementIdPart(contactBId)], anything(), anything()),
|
||||
).thenResolve([await toStorableInstance(contactAOnTheServer), await toStorableInstance(contactBOnTheServer)])
|
||||
const cache = new DefaultEntityRestCache(client, storage)
|
||||
const cache = new DefaultEntityRestCache(client, storage, typeModelResolver)
|
||||
|
||||
const cacheBypassed1 = await cache.loadMultiple(ContactTypeRef, listId, [elementIdPart(contactAId)], undefined, {
|
||||
cacheMode: CacheMode.WriteOnly,
|
||||
|
@ -2134,7 +2151,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
|
||||
const client: EntityRestClient = mockRestClient()
|
||||
when(client.loadParsedInstance(ContactTypeRef, contactId, anything())).thenResolve(await toStorableInstance(contactOnTheServer))
|
||||
const cache = new DefaultEntityRestCache(client, storage)
|
||||
const cache = new DefaultEntityRestCache(client, storage, typeModelResolver)
|
||||
|
||||
const cacheReadonly1 = await cache.load(ContactTypeRef, contactId, { cacheMode: CacheMode.ReadOnly })
|
||||
o(cacheReadonly1).deepEquals(contactOnTheServer)
|
||||
|
@ -2182,7 +2199,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
await toStorableInstance(contactBOnTheServer),
|
||||
])
|
||||
|
||||
const cache = new DefaultEntityRestCache(client, storage)
|
||||
const cache = new DefaultEntityRestCache(client, storage, typeModelResolver)
|
||||
|
||||
const cacheReadOnly1 = await cache.loadMultiple(ContactTypeRef, listId, [elementIdPart(contactAId)], undefined, {
|
||||
cacheMode: CacheMode.ReadOnly,
|
||||
|
@ -2230,7 +2247,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
await toStorableInstance(contactBOnTheServer),
|
||||
])
|
||||
|
||||
const cache = new DefaultEntityRestCache(client, storage)
|
||||
const cache = new DefaultEntityRestCache(client, storage, typeModelResolver)
|
||||
const cacheReadonly1 = await cache.loadRange(ContactTypeRef, listId, createId("0"), 2, false, { cacheMode: CacheMode.ReadOnly })
|
||||
o(cacheReadonly1).deepEquals([contactAOnTheServer, contactBOnTheServer])
|
||||
// Fresh cache; should be loaded remotely and cached
|
||||
|
@ -2277,7 +2294,7 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
when(client.loadParsedInstancesRange(ContactTypeRef, listId, createId("1"), anything(), false, anything())).thenResolve([
|
||||
await toStorableInstance(contactBOnTheServer),
|
||||
])
|
||||
const cache = new DefaultEntityRestCache(client, storage)
|
||||
const cache = new DefaultEntityRestCache(client, storage, typeModelResolver)
|
||||
const cacheReadonly1 = await cache.loadRange(ContactTypeRef, listId, createId("1"), 2, false, { cacheMode: CacheMode.ReadOnly })
|
||||
o(cacheReadonly1).deepEquals([contactBOnTheServer])
|
||||
// Fresh cache
|
||||
|
@ -2298,12 +2315,4 @@ export function testEntityRestCache(name: string, getStorage: (userId: Id) => Pr
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
function makeBatch(updates: Array<EntityUpdate>): QueuedBatch {
|
||||
return {
|
||||
events: updates,
|
||||
groupId: groupId,
|
||||
batchId: "batchId",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,13 +9,13 @@ import {
|
|||
listIdPart,
|
||||
timestampToGeneratedId,
|
||||
} from "../../../../../src/common/api/common/utils/EntityUtils.js"
|
||||
import { _verifyType, resolveClientTypeReference, resolveServerTypeReference } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { _verifyType, TypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { NotFoundError } from "../../../../../src/common/api/common/error/RestError.js"
|
||||
import { downcast, TypeRef } from "@tutao/tutanota-utils"
|
||||
import type { BlobElementEntity, ElementEntity, ListElementEntity, SomeEntity } from "../../../../../src/common/api/common/EntityTypes.js"
|
||||
import { AuthDataProvider } from "../../../../../src/common/api/worker/facades/UserFacade.js"
|
||||
import { Type } from "../../../../../src/common/api/common/EntityConstants.js"
|
||||
import { InstancePipeline } from "../../../../../src/common/api/worker/crypto/InstancePipeline"
|
||||
import { clientInitializedTypeModelResolver, instancePipelineFromTypeModelResolver } from "../../../TestUtils"
|
||||
|
||||
const authDataProvider: AuthDataProvider = {
|
||||
createAuthHeaders(): Dict {
|
||||
|
@ -31,10 +31,13 @@ export class EntityRestClientMock extends EntityRestClient {
|
|||
_listEntities: Record<Id, Record<Id, ListElementEntity | Error>> = {}
|
||||
_blobEntities: Record<Id, Record<Id, BlobElementEntity | Error>> = {}
|
||||
_lastIdTimestamp: number
|
||||
private _typeModelResolver: TypeModelResolver
|
||||
|
||||
constructor() {
|
||||
super(authDataProvider, downcast({}), () => downcast({}), new InstancePipeline(resolveClientTypeReference, resolveServerTypeReference), downcast({}))
|
||||
const typeModelResolver = clientInitializedTypeModelResolver()
|
||||
super(authDataProvider, downcast({}), () => downcast({}), instancePipelineFromTypeModelResolver(typeModelResolver), downcast({}), typeModelResolver)
|
||||
this._lastIdTimestamp = Date.now()
|
||||
this._typeModelResolver = typeModelResolver
|
||||
}
|
||||
|
||||
getNextId(): Id {
|
||||
|
@ -139,7 +142,7 @@ export class EntityRestClientMock extends EntityRestClient {
|
|||
const lid = listId
|
||||
|
||||
if (lid) {
|
||||
const typeModule = await resolveClientTypeReference(typeRef)
|
||||
const typeModule = await this._typeModelResolver.resolveClientTypeReference(typeRef)
|
||||
if (typeModule.type === Type.ListElement.valueOf()) {
|
||||
return elementIds
|
||||
.map((id) => {
|
||||
|
@ -171,7 +174,7 @@ export class EntityRestClientMock extends EntityRestClient {
|
|||
}
|
||||
|
||||
async erase<T extends SomeEntity>(instance: T): Promise<void> {
|
||||
const typeModel = await resolveClientTypeReference(instance._type)
|
||||
const typeModel = await this._typeModelResolver.resolveClientTypeReference(instance._type)
|
||||
_verifyType(typeModel)
|
||||
|
||||
const ids = getIds(instance, typeModel)
|
||||
|
@ -185,7 +188,7 @@ export class EntityRestClientMock extends EntityRestClient {
|
|||
return
|
||||
}
|
||||
|
||||
const typeModel = await resolveClientTypeReference(instances[0]._type)
|
||||
const typeModel = await this._typeModelResolver.resolveClientTypeReference(instances[0]._type)
|
||||
_verifyType(typeModel)
|
||||
|
||||
this._handleDeleteMultiple(
|
||||
|
|
|
@ -8,16 +8,9 @@ import {
|
|||
} from "../../../../../src/common/api/common/error/RestError.js"
|
||||
import { assertThrows } from "@tutao/tutanota-test-utils"
|
||||
import { SetupMultipleError } from "../../../../../src/common/api/common/error/SetupMultipleError.js"
|
||||
import {
|
||||
globalClientModelInfo,
|
||||
globalServerModelInfo,
|
||||
HttpMethod,
|
||||
MediaType,
|
||||
resolveClientTypeReference,
|
||||
resolveServerTypeReference,
|
||||
} from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { HttpMethod, MediaType, TypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { AccountingInfoTypeRef, CustomerTypeRef, GroupMemberTypeRef } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { doBlobRequestWithRetry, EntityRestClient, tryServers, typeRefToRestPath } from "../../../../../src/common/api/worker/rest/EntityRestClient.js"
|
||||
import { doBlobRequestWithRetry, EntityRestClient, tryServers, typeModelToRestPath } from "../../../../../src/common/api/worker/rest/EntityRestClient.js"
|
||||
import { RestClient } from "../../../../../src/common/api/worker/rest/RestClient.js"
|
||||
import { CryptoFacade } from "../../../../../src/common/api/worker/crypto/CryptoFacade.js"
|
||||
import { func, instance, matchers, object, verify, when } from "testdouble"
|
||||
|
@ -26,7 +19,7 @@ import sysModelInfo from "../../../../../src/common/api/entities/sys/ModelInfo.j
|
|||
import { AuthDataProvider, UserFacade } from "../../../../../src/common/api/worker/facades/UserFacade.js"
|
||||
import { LoginIncompleteError } from "../../../../../src/common/api/common/error/LoginIncompleteError.js"
|
||||
import { BlobServerAccessInfoTypeRef, BlobServerUrlTypeRef } from "../../../../../src/common/api/entities/storage/TypeRefs.js"
|
||||
import { Base64, base64ToUint8Array, deepEqual, downcast, KeyVersion, Mapper, ofClass, promiseMap, TypeRef } from "@tutao/tutanota-utils"
|
||||
import { Base64, base64ToUint8Array, deepEqual, KeyVersion, Mapper, ofClass, promiseMap, TypeRef } from "@tutao/tutanota-utils"
|
||||
import { ProgrammingError } from "../../../../../src/common/api/common/error/ProgrammingError.js"
|
||||
import { BlobAccessTokenFacade } from "../../../../../src/common/api/worker/facades/BlobAccessTokenFacade.js"
|
||||
import {
|
||||
|
@ -39,9 +32,7 @@ import {
|
|||
RecipientsTypeRef,
|
||||
SupportDataTypeRef,
|
||||
} from "../../../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { DateProvider } from "../../../../../src/common/api/common/DateProvider.js"
|
||||
import { clientModelAsServerModel, createTestEntity } from "../../../TestUtils.js"
|
||||
import { DefaultDateProvider } from "../../../../../src/common/calendar/date/CalendarUtils.js"
|
||||
import { clientInitializedTypeModelResolver, createTestEntity, instancePipelineFromTypeModelResolver } from "../../../TestUtils.js"
|
||||
import { InstancePipeline } from "../../../../../src/common/api/worker/crypto/InstancePipeline"
|
||||
import { type Entity, TypeModel } from "../../../../../src/common/api/common/EntityTypes"
|
||||
import { PersistenceResourcePostReturnTypeRef } from "../../../../../src/common/api/entities/base/TypeRefs"
|
||||
|
@ -98,24 +89,27 @@ o.spec("EntityRestClient", function () {
|
|||
let cryptoFacadePartialStub: CryptoFacade
|
||||
let fullyLoggedIn: boolean
|
||||
let blobAccessTokenFacade: BlobAccessTokenFacade
|
||||
let dateProvider: DateProvider
|
||||
const keyLoaderFacadeMock = instance(KeyLoaderFacade)
|
||||
const ownerGroupId = "ownerGroupId"
|
||||
let sk: AesKey
|
||||
let ownerGroupKey: VersionedKey
|
||||
let encryptedSessionKey
|
||||
let currentDebuggingStatus
|
||||
let typeModelResolver: TypeModelResolver
|
||||
|
||||
async function typeRefToRestPath(typeRef: TypeRef<unknown>): Promise<string> {
|
||||
return typeModelToRestPath(await typeModelResolver.resolveClientTypeReference(typeRef))
|
||||
}
|
||||
|
||||
o.beforeEach(function () {
|
||||
currentDebuggingStatus = env.networkDebugging
|
||||
instancePipeline = new InstancePipeline(resolveClientTypeReference, resolveServerTypeReference)
|
||||
typeModelResolver = clientInitializedTypeModelResolver()
|
||||
instancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
|
||||
// instead of mocking the instance pipeline itself, mock it's internal mapper.
|
||||
blobAccessTokenFacade = instance(BlobAccessTokenFacade)
|
||||
|
||||
restClient = object()
|
||||
|
||||
clientModelAsServerModel(globalServerModelInfo, globalClientModelInfo)
|
||||
|
||||
sk = aes256RandomKey()
|
||||
ownerGroupKey = { object: aes256RandomKey(), version: 0 }
|
||||
encryptedSessionKey = encryptKeyWithVersionedKey(ownerGroupKey, sk)
|
||||
|
@ -135,6 +129,7 @@ o.spec("EntityRestClient", function () {
|
|||
async () => instance(KeyVerificationFacade),
|
||||
instance(PublicKeyProvider),
|
||||
() => instance(KeyRotationFacade),
|
||||
typeModelResolver,
|
||||
)
|
||||
cryptoFacadePartialStub.resolveSessionKey = async (instance: Entity): Promise<Nullable<AesKey>> => {
|
||||
return sk
|
||||
|
@ -149,8 +144,14 @@ o.spec("EntityRestClient", function () {
|
|||
},
|
||||
}
|
||||
|
||||
dateProvider = instance(DefaultDateProvider)
|
||||
entityRestClient = new EntityRestClient(authDataProvider, restClient, () => cryptoFacadePartialStub, instancePipeline, blobAccessTokenFacade)
|
||||
entityRestClient = new EntityRestClient(
|
||||
authDataProvider,
|
||||
restClient,
|
||||
() => cryptoFacadePartialStub,
|
||||
instancePipeline,
|
||||
blobAccessTokenFacade,
|
||||
typeModelResolver,
|
||||
)
|
||||
})
|
||||
|
||||
o.afterEach(() => {
|
||||
|
@ -770,7 +771,7 @@ o.spec("EntityRestClient", function () {
|
|||
|
||||
o.spec("Setup", function () {
|
||||
o("Setup list entity", async function () {
|
||||
const v = (await resolveClientTypeReference(CalendarEventTypeRef)).version
|
||||
const v = (await typeModelResolver.resolveClientTypeReference(CalendarEventTypeRef)).version
|
||||
const ownerGroupKey: VersionedKey = { object: aes256RandomKey(), version: 0 }
|
||||
const newCalendar = createTestEntity(CalendarEventTypeRef, {
|
||||
_id: ["listId", "element"],
|
||||
|
@ -797,7 +798,7 @@ o.spec("EntityRestClient", function () {
|
|||
AttributeModel.getAttribute<Base64>(
|
||||
untypedInstance,
|
||||
"_ownerEncSessionKey",
|
||||
await resolveClientTypeReference(AccountingInfoTypeRef),
|
||||
await typeModelResolver.resolveClientTypeReference(AccountingInfoTypeRef),
|
||||
),
|
||||
)
|
||||
const sk = decryptKey(ownerGroupKey.object, ownerEncSk)
|
||||
|
@ -820,7 +821,7 @@ o.spec("EntityRestClient", function () {
|
|||
})
|
||||
|
||||
o("Setup entity", async function () {
|
||||
const v = (await resolveClientTypeReference(SupportDataTypeRef)).version
|
||||
const v = (await typeModelResolver.resolveClientTypeReference(SupportDataTypeRef)).version
|
||||
const newSupportData = createTestEntity(SupportDataTypeRef, {
|
||||
_id: "1",
|
||||
_permissions: "another id",
|
||||
|
@ -886,7 +887,7 @@ o.spec("EntityRestClient", function () {
|
|||
})
|
||||
|
||||
o("when ownerKey is passed it is used instead for session key resolution", async function () {
|
||||
const typeModel = await resolveClientTypeReference(AccountingInfoTypeRef)
|
||||
const typeModel = await typeModelResolver.resolveClientTypeReference(AccountingInfoTypeRef)
|
||||
const v = typeModel.version
|
||||
const ownerGroupKey: VersionedKey = { object: aes256RandomKey(), version: 0 }
|
||||
const newAccountingInfo = createTestEntity(AccountingInfoTypeRef, {
|
||||
|
@ -915,7 +916,7 @@ o.spec("EntityRestClient", function () {
|
|||
AttributeModel.getAttribute<Base64>(
|
||||
untypedInstance,
|
||||
"_ownerEncSessionKey",
|
||||
await resolveClientTypeReference(AccountingInfoTypeRef),
|
||||
await typeModelResolver.resolveClientTypeReference(AccountingInfoTypeRef),
|
||||
),
|
||||
)
|
||||
const sk = decryptKey(ownerGroupKey.object, ownerEncSk)
|
||||
|
@ -936,7 +937,7 @@ o.spec("EntityRestClient", function () {
|
|||
o.spec("Setup multiple", function () {
|
||||
o("Less than 100 entities created should result in a single rest request", async function () {
|
||||
const newGroupMembers = groupMembers(1)
|
||||
const { version } = await resolveClientTypeReference(GroupMemberTypeRef)
|
||||
const { version } = await typeModelResolver.resolveClientTypeReference(GroupMemberTypeRef)
|
||||
const resultId = "resultId"
|
||||
|
||||
const untypedGroupMembers = await promiseMap(newGroupMembers, async (group) => {
|
||||
|
@ -967,7 +968,7 @@ o.spec("EntityRestClient", function () {
|
|||
o("Exactly 100 entities created should result in a single rest request", async function () {
|
||||
const newGroupMembers = groupMembers(100)
|
||||
const resultIds = countFrom(0, 100).map(String)
|
||||
const { version } = await resolveClientTypeReference(GroupMemberTypeRef)
|
||||
const { version } = await typeModelResolver.resolveClientTypeReference(GroupMemberTypeRef)
|
||||
const untypedGroupMembers = await promiseMap(newGroupMembers, async (group) => {
|
||||
return instancePipeline.mapAndEncrypt(GroupMemberTypeRef, group, null)
|
||||
})
|
||||
|
@ -997,7 +998,7 @@ o.spec("EntityRestClient", function () {
|
|||
o("More than 100 entities created should result in 2 rest requests", async function () {
|
||||
const newGroupMembers = groupMembers(101)
|
||||
const resultIds = countFrom(0, 101).map(String)
|
||||
const { version } = await resolveClientTypeReference(GroupMemberTypeRef)
|
||||
const { version } = await typeModelResolver.resolveClientTypeReference(GroupMemberTypeRef)
|
||||
const untypedGroupMembers = await promiseMap(newGroupMembers, async (group) => {
|
||||
return instancePipeline.mapAndEncrypt(GroupMemberTypeRef, group, null)
|
||||
})
|
||||
|
@ -1048,7 +1049,7 @@ o.spec("EntityRestClient", function () {
|
|||
o("Post multiple: An error is encountered for part of the request, only failed entities are returned in the result", async function () {
|
||||
const newGroupMembers = groupMembers(400)
|
||||
const resultIds = countFrom(0, 400).map(String)
|
||||
const { version } = await resolveClientTypeReference(GroupMemberTypeRef)
|
||||
const { version } = await typeModelResolver.resolveClientTypeReference(GroupMemberTypeRef)
|
||||
const untypedGroupMembers = await promiseMap(newGroupMembers, async (group) => {
|
||||
return instancePipeline.mapAndEncrypt(GroupMemberTypeRef, group, null)
|
||||
})
|
||||
|
@ -1120,7 +1121,7 @@ o.spec("EntityRestClient", function () {
|
|||
|
||||
o.spec("Update", function () {
|
||||
o("Update entity", async function () {
|
||||
const { version } = await resolveClientTypeReference(SupportDataTypeRef)
|
||||
const { version } = await typeModelResolver.resolveClientTypeReference(SupportDataTypeRef)
|
||||
const newSupportData = createTestEntity(SupportDataTypeRef, {
|
||||
_id: "id",
|
||||
})
|
||||
|
@ -1142,7 +1143,7 @@ o.spec("EntityRestClient", function () {
|
|||
})
|
||||
|
||||
o("when ownerKey is passed it is used instead for session key resolution", async function () {
|
||||
const typeModel = await resolveClientTypeReference(AccountingInfoTypeRef)
|
||||
const typeModel = await typeModelResolver.resolveClientTypeReference(AccountingInfoTypeRef)
|
||||
const version = typeModel.version
|
||||
const ownerKeyProviderSk = aes256RandomKey()
|
||||
const ownerGroupKey: VersionedKey = { object: aes256RandomKey(), version: 0 }
|
||||
|
@ -1184,7 +1185,7 @@ o.spec("EntityRestClient", function () {
|
|||
|
||||
o.spec("Delete", function () {
|
||||
o("Delete entity", async function () {
|
||||
const { version } = await resolveClientTypeReference(CustomerTypeRef)
|
||||
const { version } = await typeModelResolver.resolveClientTypeReference(CustomerTypeRef)
|
||||
const id = "id"
|
||||
const newCustomer = createTestEntity(CustomerTypeRef, {
|
||||
_id: id,
|
||||
|
@ -1199,7 +1200,7 @@ o.spec("EntityRestClient", function () {
|
|||
})
|
||||
|
||||
o("Delete entities", async function () {
|
||||
const { version } = await resolveClientTypeReference(CustomerTypeRef)
|
||||
const { version } = await typeModelResolver.resolveClientTypeReference(CustomerTypeRef)
|
||||
const id = "id"
|
||||
const idTwo = "id2"
|
||||
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
import o from "@tutao/otest"
|
||||
import { EphemeralCacheStorage } from "../../../../../src/common/api/worker/rest/EphemeralCacheStorage.js"
|
||||
import { BodyTypeRef, MailDetailsBlobTypeRef, MailDetailsTypeRef, RecipientsTypeRef } from "../../../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { clientModelAsServerModel, createTestEntity } from "../../../TestUtils.js"
|
||||
import { clientInitializedTypeModelResolver, createTestEntity, modelMapperFromTypeModelResolver } from "../../../TestUtils.js"
|
||||
import { ModelMapper } from "../../../../../src/common/api/worker/crypto/ModelMapper"
|
||||
import {
|
||||
globalClientModelInfo,
|
||||
globalServerModelInfo,
|
||||
resolveClientTypeReference,
|
||||
resolveServerTypeReference,
|
||||
} from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { ServerModelParsedInstance } from "../../../../../src/common/api/common/EntityTypes"
|
||||
import { TypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
o.spec("EphemeralCacheStorageTest", function () {
|
||||
const userId = "userId"
|
||||
const archiveId = "archiveId"
|
||||
const blobElementId = "blobElementId1"
|
||||
const modelMapper = new ModelMapper(resolveClientTypeReference, resolveServerTypeReference)
|
||||
let typeModelResolver: TypeModelResolver
|
||||
let modelMapper: ModelMapper
|
||||
let storage: EphemeralCacheStorage
|
||||
|
||||
clientModelAsServerModel(globalServerModelInfo, globalClientModelInfo)
|
||||
|
||||
const storage = new EphemeralCacheStorage(modelMapper)
|
||||
o.beforeEach(() => {
|
||||
typeModelResolver = clientInitializedTypeModelResolver()
|
||||
modelMapper = modelMapperFromTypeModelResolver(typeModelResolver)
|
||||
storage = new EphemeralCacheStorage(modelMapper, typeModelResolver)
|
||||
})
|
||||
|
||||
o.spec("BlobElementType", function () {
|
||||
o("cache roundtrip: put, get, delete", async function () {
|
||||
|
|
|
@ -5,13 +5,13 @@ import { CryptoFacade } from "../../../../../src/common/api/worker/crypto/Crypto
|
|||
import { matchers, object, when } from "testdouble"
|
||||
import { DeleteService, GetService, PostService, PutService } from "../../../../../src/common/api/common/ServiceRequest.js"
|
||||
import { AlarmServicePostTypeRef, GiftCardCreateDataTypeRef, SaltDataTypeRef } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { HttpMethod, MediaType, resolveClientTypeReference, resolveServerTypeReference } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { HttpMethod, MediaType, TypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { deepEqual, downcast } from "@tutao/tutanota-utils"
|
||||
import { assertThrows, verify } from "@tutao/tutanota-test-utils"
|
||||
import { ProgrammingError } from "../../../../../src/common/api/common/error/ProgrammingError"
|
||||
import { AuthDataProvider } from "../../../../../src/common/api/worker/facades/UserFacade"
|
||||
import { LoginIncompleteError } from "../../../../../src/common/api/common/error/LoginIncompleteError.js"
|
||||
import { createTestEntity } from "../../../TestUtils.js"
|
||||
import { clientInitializedTypeModelResolver, createTestEntity, instancePipelineFromTypeModelResolver } from "../../../TestUtils.js"
|
||||
import { InstancePipeline } from "../../../../../src/common/api/worker/crypto/InstancePipeline"
|
||||
import { CustomerAccountReturnTypeRef } from "../../../../../src/common/api/entities/accounting/TypeRefs"
|
||||
import { aes256RandomKey } from "@tutao/tutanota-crypto"
|
||||
|
@ -33,6 +33,7 @@ o.spec("ServiceExecutor", function () {
|
|||
let executor: ServiceExecutor
|
||||
let fullyLoggedIn: boolean = true
|
||||
let previousNetworkDebugging
|
||||
let typeModelResolver: TypeModelResolver
|
||||
const authDataProvider: AuthDataProvider = {
|
||||
createAuthHeaders(): Dict {
|
||||
return authHeaders
|
||||
|
@ -49,7 +50,8 @@ o.spec("ServiceExecutor", function () {
|
|||
|
||||
instancePipeline = object()
|
||||
cryptoFacade = object()
|
||||
executor = new ServiceExecutor(restClient, authDataProvider, instancePipeline, () => cryptoFacade)
|
||||
typeModelResolver = clientInitializedTypeModelResolver()
|
||||
executor = new ServiceExecutor(restClient, authDataProvider, instancePipeline, () => cryptoFacade, typeModelResolver)
|
||||
previousNetworkDebugging = env.networkDebugging
|
||||
env.networkDebugging = false
|
||||
})
|
||||
|
@ -68,7 +70,7 @@ o.spec("ServiceExecutor", function () {
|
|||
o("decryptResponse removes network debugging info", async function () {
|
||||
env.networkDebugging = true
|
||||
|
||||
const realInstancePipeline = new InstancePipeline(resolveClientTypeReference, resolveServerTypeReference)
|
||||
const realInstancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
|
||||
|
||||
const getService: GetService & DeleteService & PutService & PostService = {
|
||||
...service,
|
||||
|
@ -78,7 +80,6 @@ o.spec("ServiceExecutor", function () {
|
|||
delete: { data: null, return: SaltDataTypeRef },
|
||||
}
|
||||
|
||||
const saltDataTypeModel = await resolveServerTypeReference(SaltDataTypeRef)
|
||||
const expectedInstance = createTestEntity(SaltDataTypeRef, { mailAddress: "test" })
|
||||
const dataWithDebug = await realInstancePipeline.mapAndEncrypt(SaltDataTypeRef, expectedInstance, null)
|
||||
|
||||
|
@ -515,7 +516,7 @@ o.spec("ServiceExecutor", function () {
|
|||
}
|
||||
const data = createTestEntity(SaltDataTypeRef, { mailAddress: "test" })
|
||||
const headers = Object.freeze({ myHeader: "2" })
|
||||
const saltTypeModel = await resolveClientTypeReference(SaltDataTypeRef)
|
||||
const saltTypeModel = await typeModelResolver.resolveClientTypeReference(SaltDataTypeRef)
|
||||
when(instancePipeline.mapAndEncrypt(anything(), anything(), anything())).thenResolve({})
|
||||
respondWith(undefined)
|
||||
|
||||
|
@ -548,7 +549,7 @@ o.spec("ServiceExecutor", function () {
|
|||
const data = createTestEntity(SaltDataTypeRef, { mailAddress: "test" })
|
||||
const accessToken = "myAccessToken"
|
||||
authHeaders = { accessToken }
|
||||
const saltTypeModel = await resolveClientTypeReference(SaltDataTypeRef)
|
||||
const saltTypeModel = await typeModelResolver.resolveClientTypeReference(SaltDataTypeRef)
|
||||
when(instancePipeline.mapAndEncrypt(anything(), anything(), anything())).thenResolve({})
|
||||
respondWith(undefined)
|
||||
|
||||
|
@ -572,8 +573,8 @@ o.spec("ServiceExecutor", function () {
|
|||
|
||||
o.spec("keys decrypt", function () {
|
||||
o.beforeEach(() => {
|
||||
instancePipeline = new InstancePipeline(resolveClientTypeReference, resolveServerTypeReference)
|
||||
executor = new ServiceExecutor(restClient, authDataProvider, instancePipeline, () => cryptoFacade)
|
||||
instancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
|
||||
executor = new ServiceExecutor(restClient, authDataProvider, instancePipeline, () => cryptoFacade, typeModelResolver)
|
||||
})
|
||||
|
||||
o("uses resolved key to decrypt response x", async function () {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import o from "@tutao/otest"
|
||||
import {
|
||||
Contact,
|
||||
ContactAddressTypeRef,
|
||||
ContactListTypeRef,
|
||||
ContactMailAddressTypeRef,
|
||||
|
@ -12,26 +13,29 @@ import { NotAuthorizedError, NotFoundError } from "../../../../../src/common/api
|
|||
import { DbTransaction } from "../../../../../src/common/api/worker/search/DbFacade.js"
|
||||
import { FULL_INDEXED_TIMESTAMP, NOTHING_INDEXED_TIMESTAMP, OperationType } from "../../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import { _createNewIndexUpdate, encryptIndexKeyBase64, typeRefToTypeInfo } from "../../../../../src/common/api/worker/search/IndexUtils.js"
|
||||
import type { EntityUpdate } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { EntityUpdateTypeRef } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { createTestEntity, makeCore } from "../../../TestUtils.js"
|
||||
import { assertNotNull, downcast } from "@tutao/tutanota-utils"
|
||||
import { downcast } from "@tutao/tutanota-utils"
|
||||
import { isSameId } from "../../../../../src/common/api/common/utils/EntityUtils.js"
|
||||
import { fixedIv } from "@tutao/tutanota-crypto"
|
||||
import { resolveClientTypeReference } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { GroupDataOS } from "../../../../../src/common/api/worker/search/IndexTables.js"
|
||||
import { spy } from "@tutao/tutanota-test-utils"
|
||||
import { AttributeModel } from "../../../../../src/common/api/common/AttributeModel"
|
||||
import { SuggestionFacade } from "../../../../../src/mail-app/workerUtils/index/SuggestionFacade"
|
||||
import { ClientModelInfo, ClientTypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { EntityUpdateData } from "../../../../../src/common/api/common/utils/EntityUpdateUtils"
|
||||
|
||||
const dbMock: any = { iv: fixedIv }
|
||||
const contactTypeInfo = typeRefToTypeInfo(ContactTypeRef)
|
||||
|
||||
o.spec("ContactIndexer test", () => {
|
||||
let suggestionFacadeMock
|
||||
let suggestionFacadeMock: SuggestionFacade<Contact>
|
||||
let clientTypeModelResolver: ClientTypeModelResolver
|
||||
|
||||
o.beforeEach(function () {
|
||||
suggestionFacadeMock = {} as any
|
||||
suggestionFacadeMock.addSuggestions = spy()
|
||||
suggestionFacadeMock.store = spy(() => Promise.resolve())
|
||||
clientTypeModelResolver = ClientModelInfo.getNewInstanceForTestsOnly()
|
||||
})
|
||||
|
||||
o("createContactIndexEntries without entries", function () {
|
||||
|
@ -94,7 +98,7 @@ o.spec("ContactIndexer test", () => {
|
|||
let attributes = attributeHandlers.map((h) => {
|
||||
return { attribute: h.attribute.id, value: h.value() }
|
||||
})
|
||||
const ContactModel = await resolveClientTypeReference(ContactTypeRef)
|
||||
const ContactModel = await clientTypeModelResolver.resolveClientTypeReference(ContactTypeRef)
|
||||
o(attributes).deepEquals([
|
||||
{ attribute: AttributeModel.getModelValue(ContactModel, "firstName").id, value: "FN" },
|
||||
{ attribute: AttributeModel.getModelValue(ContactModel, "lastName").id, value: "LN" },
|
||||
|
@ -120,13 +124,10 @@ o.spec("ContactIndexer test", () => {
|
|||
} as any
|
||||
|
||||
const contactIndexer = new ContactIndexer(indexer, dbMock, entity, suggestionFacadeMock)
|
||||
let event: EntityUpdate = { instanceListId: "lid", instanceId: "eid" } as any
|
||||
let event: EntityUpdateData = { instanceListId: "lid", instanceId: "eid" } as any
|
||||
const result = await contactIndexer.processNewContact(event)
|
||||
// @ts-ignore
|
||||
o(result).deepEquals({ contact, keyToIndexEntries })
|
||||
// @ts-ignore
|
||||
o(contactIndexer._entity.load.args[0]).equals(ContactTypeRef)
|
||||
// @ts-ignore
|
||||
o(contactIndexer._entity.load.args[1]).deepEquals([event.instanceListId, event.instanceId])
|
||||
o(suggestionFacadeMock.addSuggestions.callCount).equals(1)
|
||||
o(suggestionFacadeMock.addSuggestions.args[0].join(",")).equals("")
|
||||
|
@ -141,7 +142,7 @@ o.spec("ContactIndexer test", () => {
|
|||
load: () => Promise.reject(new NotFoundError("blah")),
|
||||
} as any
|
||||
const contactIndexer = new ContactIndexer(core, dbMock, entity, suggestionFacadeMock)
|
||||
let event: EntityUpdate = { instanceListId: "lid", instanceId: "eid" } as any
|
||||
let event: EntityUpdateData = { instanceListId: "lid", instanceId: "eid" } as any
|
||||
return contactIndexer.processNewContact(event).then((result) => {
|
||||
o(result).equals(null)
|
||||
o(suggestionFacadeMock.addSuggestions.callCount).equals(0)
|
||||
|
@ -156,7 +157,7 @@ o.spec("ContactIndexer test", () => {
|
|||
load: () => Promise.reject(new NotAuthorizedError("blah")),
|
||||
} as any
|
||||
const contactIndexer = new ContactIndexer(indexer, dbMock, entity, suggestionFacadeMock)
|
||||
let event: EntityUpdate = { instanceListId: "lid", instanceId: "eid" } as any
|
||||
let event: EntityUpdateData = { instanceListId: "lid", instanceId: "eid" } as any
|
||||
return contactIndexer.processNewContact(event).then((result) => {
|
||||
o(result).equals(null)
|
||||
o(suggestionFacadeMock.addSuggestions.callCount).equals(0)
|
||||
|
@ -171,7 +172,7 @@ o.spec("ContactIndexer test", () => {
|
|||
load: () => Promise.reject(new Error("blah")),
|
||||
} as any
|
||||
const contactIndexer = new ContactIndexer(core, dbMock, entity, suggestionFacadeMock)
|
||||
let event: EntityUpdate = { instanceListId: "lid", instanceId: "eid" } as any
|
||||
let event: EntityUpdateData = { instanceListId: "lid", instanceId: "eid" } as any
|
||||
await contactIndexer.processNewContact(event).catch((e) => {
|
||||
o(suggestionFacadeMock.addSuggestions.callCount).equals(0)
|
||||
})
|
||||
|
@ -249,7 +250,7 @@ o.spec("ContactIndexer test", () => {
|
|||
const indexer = new ContactIndexer(core, core.db, entity, suggestionFacadeMock)
|
||||
|
||||
let indexUpdate = _createNewIndexUpdate(contactTypeInfo)
|
||||
let events = [createUpdate(OperationType.CREATE, "contact-list", "L-dNNLe----0")]
|
||||
let events: EntityUpdateData[] = [createUpdate(OperationType.CREATE, "contact-list", "L-dNNLe----0")]
|
||||
await indexer.processEntityEvents(events, "group-id", "batch-id", indexUpdate)
|
||||
// nothing changed
|
||||
o(indexUpdate.create.encInstanceIdToElementData.size).equals(1)
|
||||
|
@ -313,10 +314,10 @@ o.spec("ContactIndexer test", () => {
|
|||
})
|
||||
})
|
||||
|
||||
function createUpdate(type: OperationType, listId: Id, id: Id) {
|
||||
let update = createTestEntity(EntityUpdateTypeRef)
|
||||
update.operation = type
|
||||
update.instanceListId = listId
|
||||
update.instanceId = id
|
||||
return update
|
||||
function createUpdate(type: OperationType, listId: Id, id: Id): EntityUpdateData {
|
||||
return {
|
||||
operation: type,
|
||||
instanceId: id,
|
||||
instanceListId: listId,
|
||||
} as Partial<EntityUpdateData> as EntityUpdateData
|
||||
}
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
import o from "@tutao/otest"
|
||||
import { batchMod, EntityModificationType, EventQueue, QueuedBatch } from "../../../../../src/common/api/worker/EventQueue.js"
|
||||
import { EntityUpdate, EntityUpdateTypeRef, GroupTypeRef } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { GroupTypeRef } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { OperationType } from "../../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import { defer, delay } from "@tutao/tutanota-utils"
|
||||
import { ConnectionError } from "../../../../../src/common/api/common/error/RestError.js"
|
||||
import { ContactTypeRef, MailboxGroupRootTypeRef, MailTypeRef } from "../../../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { spy } from "@tutao/tutanota-test-utils"
|
||||
import { createTestEntity } from "../../../TestUtils.js"
|
||||
import { EntityUpdateData } from "../../../../../src/common/api/common/utils/EntityUpdateUtils"
|
||||
|
||||
o.spec("EventQueueTest", function () {
|
||||
let queue: EventQueue
|
||||
let processElement: any
|
||||
let lastProcess: { resolve: () => void; reject: (Error) => void; promise: Promise<void> }
|
||||
|
||||
const newUpdate = (type: OperationType, instanceId: string) => {
|
||||
const update = createTestEntity(EntityUpdateTypeRef)
|
||||
update.operation = type
|
||||
update.instanceId = instanceId
|
||||
return update
|
||||
const newUpdate = (type: OperationType, instanceId: string): EntityUpdateData => {
|
||||
return {
|
||||
operation: type,
|
||||
instanceId,
|
||||
instanceListId: "",
|
||||
typeRef: MailTypeRef,
|
||||
} as Partial<EntityUpdateData> as EntityUpdateData
|
||||
}
|
||||
|
||||
o.beforeEach(function () {
|
||||
|
@ -107,15 +109,15 @@ o.spec("EventQueueTest", function () {
|
|||
})
|
||||
|
||||
o("create + delete == delete", async function () {
|
||||
const createEvent = createUpdate(OperationType.CREATE, "new-mail-list", "1", "u1")
|
||||
const deleteEvent = createUpdate(OperationType.DELETE, createEvent.instanceListId, createEvent.instanceId, "u2")
|
||||
const createEvent = createUpdate(OperationType.CREATE, "new-mail-list", "1")
|
||||
const deleteEvent = createUpdate(OperationType.DELETE, createEvent.instanceListId, createEvent.instanceId)
|
||||
|
||||
queue.add("batch-id-1", "group-id", [createEvent])
|
||||
queue.add("batch-id-2", "group-id", [deleteEvent])
|
||||
queue.resume()
|
||||
await lastProcess.promise
|
||||
|
||||
const expectedDelete = createUpdate(OperationType.DELETE, createEvent.instanceListId, createEvent.instanceId, "u2")
|
||||
const expectedDelete = createUpdate(OperationType.DELETE, createEvent.instanceListId, createEvent.instanceId)
|
||||
|
||||
o(processElement.invocations).deepEquals([
|
||||
[{ events: [], batchId: "batch-id-1", groupId: "group-id" }],
|
||||
|
@ -124,15 +126,15 @@ o.spec("EventQueueTest", function () {
|
|||
})
|
||||
|
||||
o("create + update == create", async function () {
|
||||
const createEvent = createUpdate(OperationType.CREATE, "new-mail-list", "1", "u1")
|
||||
const updateEvent = createUpdate(OperationType.UPDATE, createEvent.instanceListId, createEvent.instanceId, "u2")
|
||||
const createEvent = createUpdate(OperationType.CREATE, "new-mail-list", "1")
|
||||
const updateEvent = createUpdate(OperationType.UPDATE, createEvent.instanceListId, createEvent.instanceId)
|
||||
|
||||
queue.add("batch-id-1", "group-id", [createEvent])
|
||||
queue.add("batch-id-2", "group-id", [updateEvent])
|
||||
queue.resume()
|
||||
await lastProcess.promise
|
||||
|
||||
const expectedCreate = createUpdate(OperationType.CREATE, createEvent.instanceListId, createEvent.instanceId, "u1")
|
||||
const expectedCreate = createUpdate(OperationType.CREATE, createEvent.instanceListId, createEvent.instanceId)
|
||||
|
||||
o(processElement.invocations).deepEquals([
|
||||
[{ events: [expectedCreate], batchId: "batch-id-1", groupId: "group-id" }],
|
||||
|
@ -141,16 +143,16 @@ o.spec("EventQueueTest", function () {
|
|||
})
|
||||
|
||||
o("create + create == create + create", async function () {
|
||||
const createEvent = createUpdate(OperationType.CREATE, "new-mail-list", "1", "u1")
|
||||
const createEvent2 = createUpdate(OperationType.CREATE, createEvent.instanceListId, createEvent.instanceId, "u2")
|
||||
const createEvent = createUpdate(OperationType.CREATE, "new-mail-list", "1")
|
||||
const createEvent2 = createUpdate(OperationType.CREATE, createEvent.instanceListId, createEvent.instanceId)
|
||||
|
||||
queue.add("batch-id-1", "group-id", [createEvent])
|
||||
queue.add("batch-id-2", "group-id", [createEvent2])
|
||||
queue.resume()
|
||||
await lastProcess.promise
|
||||
|
||||
const expectedCreate = createUpdate(OperationType.CREATE, createEvent.instanceListId, createEvent.instanceId, "u1")
|
||||
const expectedCreate2 = createUpdate(OperationType.CREATE, createEvent.instanceListId, createEvent.instanceId, "u2")
|
||||
const expectedCreate = createUpdate(OperationType.CREATE, createEvent.instanceListId, createEvent.instanceId)
|
||||
const expectedCreate2 = createUpdate(OperationType.CREATE, createEvent.instanceListId, createEvent.instanceId)
|
||||
|
||||
o(processElement.invocations).deepEquals([
|
||||
[{ events: [expectedCreate], batchId: "batch-id-1", groupId: "group-id" }],
|
||||
|
@ -159,9 +161,9 @@ o.spec("EventQueueTest", function () {
|
|||
})
|
||||
|
||||
o("create + update + delete == delete", async function () {
|
||||
const createEvent = createUpdate(OperationType.CREATE, "new-mail-list", "1", "u1")
|
||||
const updateEvent = createUpdate(OperationType.UPDATE, "new-mail-list", "1", "u2")
|
||||
const deleteEvent = createUpdate(OperationType.DELETE, createEvent.instanceListId, createEvent.instanceId, "u")
|
||||
const createEvent = createUpdate(OperationType.CREATE, "new-mail-list", "1")
|
||||
const updateEvent = createUpdate(OperationType.UPDATE, "new-mail-list", "1")
|
||||
const deleteEvent = createUpdate(OperationType.DELETE, createEvent.instanceListId, createEvent.instanceId)
|
||||
|
||||
queue.add("batch-id-1", "group-id", [createEvent])
|
||||
queue.add("batch-id-2", "group-id", [updateEvent])
|
||||
|
@ -169,7 +171,7 @@ o.spec("EventQueueTest", function () {
|
|||
queue.resume()
|
||||
await lastProcess.promise
|
||||
|
||||
const expectedDelete = createUpdate(OperationType.DELETE, createEvent.instanceListId, createEvent.instanceId, "u")
|
||||
const expectedDelete = createUpdate(OperationType.DELETE, createEvent.instanceListId, createEvent.instanceId)
|
||||
|
||||
o(processElement.invocations).deepEquals([
|
||||
[{ events: [], batchId: "batch-id-1", groupId: "group-id" }],
|
||||
|
@ -180,8 +182,8 @@ o.spec("EventQueueTest", function () {
|
|||
|
||||
o("delete + create == delete + create", async function () {
|
||||
// DELETE can happen after CREATE in case of custom id. We keep it as-is
|
||||
const deleteEvent = createUpdate(OperationType.DELETE, "mail-list", "1", "u0")
|
||||
const createEvent = createUpdate(OperationType.CREATE, "mail-list", "1", "u1")
|
||||
const deleteEvent = createUpdate(OperationType.DELETE, "mail-list", "1")
|
||||
const createEvent = createUpdate(OperationType.CREATE, "mail-list", "1")
|
||||
|
||||
queue.add("batch-id-0", "group-id", [deleteEvent])
|
||||
queue.add("batch-id-1", "group-id", [createEvent])
|
||||
|
@ -196,12 +198,12 @@ o.spec("EventQueueTest", function () {
|
|||
|
||||
o("delete + create + delete + create == delete + create", async function () {
|
||||
// This tests that create still works a
|
||||
const deleteEvent1 = createUpdate(OperationType.DELETE, "list", "1", "u1")
|
||||
const nonEmptyEventInBetween = createUpdate(OperationType.CREATE, "list2", "2", "u1.1")
|
||||
const createEvent1 = createUpdate(OperationType.CREATE, "list", "1", "u2")
|
||||
const deleteEvent1 = createUpdate(OperationType.DELETE, "list", "1")
|
||||
const nonEmptyEventInBetween = createUpdate(OperationType.CREATE, "list2", "2")
|
||||
const createEvent1 = createUpdate(OperationType.CREATE, "list", "1")
|
||||
|
||||
const deleteEvent2 = createUpdate(OperationType.DELETE, "list", "1", "u3")
|
||||
const createEvent2 = createUpdate(OperationType.CREATE, "list", "1", "u4")
|
||||
const deleteEvent2 = createUpdate(OperationType.DELETE, "list", "1")
|
||||
const createEvent2 = createUpdate(OperationType.CREATE, "list", "1")
|
||||
|
||||
queue.add("batch-id-1", "group-id", [deleteEvent1])
|
||||
queue.add("batch-id-1.1", "group-id", [nonEmptyEventInBetween])
|
||||
|
@ -211,9 +213,9 @@ o.spec("EventQueueTest", function () {
|
|||
queue.resume()
|
||||
await lastProcess.promise
|
||||
|
||||
const expectedDelete = createUpdate(OperationType.DELETE, createEvent1.instanceListId, createEvent1.instanceId, "u1")
|
||||
const expectedCreate = createUpdate(OperationType.CREATE, createEvent1.instanceListId, createEvent1.instanceId, "u4")
|
||||
const expectedDelete2 = createUpdate(OperationType.DELETE, createEvent1.instanceListId, createEvent1.instanceId, "u3")
|
||||
const expectedDelete = createUpdate(OperationType.DELETE, createEvent1.instanceListId, createEvent1.instanceId)
|
||||
const expectedCreate = createUpdate(OperationType.CREATE, createEvent1.instanceListId, createEvent1.instanceId)
|
||||
const expectedDelete2 = createUpdate(OperationType.DELETE, createEvent1.instanceListId, createEvent1.instanceId)
|
||||
|
||||
o(processElement.invocations).deepEquals([
|
||||
[{ events: [expectedDelete], batchId: "batch-id-1", groupId: "group-id" }],
|
||||
|
@ -226,16 +228,16 @@ o.spec("EventQueueTest", function () {
|
|||
|
||||
o("delete (list 1) + create (list 2) == delete (list 1) + create (list 2)", async function () {
|
||||
// entity updates with for the same element id but different list IDs do not influence each other
|
||||
const deleteEvent1 = createUpdate(OperationType.DELETE, "list1", "1", "u1")
|
||||
const createEvent1 = createUpdate(OperationType.CREATE, "list2", "1", "u2")
|
||||
const deleteEvent1 = createUpdate(OperationType.DELETE, "list1", "1")
|
||||
const createEvent1 = createUpdate(OperationType.CREATE, "list2", "1")
|
||||
|
||||
queue.add("batch-id-1", "group-id", [deleteEvent1])
|
||||
queue.add("batch-id-2", "group-id", [createEvent1])
|
||||
queue.resume()
|
||||
await lastProcess.promise
|
||||
|
||||
const expectedDelete = createUpdate(OperationType.DELETE, deleteEvent1.instanceListId, deleteEvent1.instanceId, "u1")
|
||||
const expectedCreate = createUpdate(OperationType.CREATE, createEvent1.instanceListId, createEvent1.instanceId, "u2")
|
||||
const expectedDelete = createUpdate(OperationType.DELETE, deleteEvent1.instanceListId, deleteEvent1.instanceId)
|
||||
const expectedCreate = createUpdate(OperationType.CREATE, createEvent1.instanceListId, createEvent1.instanceId)
|
||||
|
||||
o(processElement.invocations).deepEquals([
|
||||
[{ events: [expectedDelete], batchId: "batch-id-1", groupId: "group-id" }],
|
||||
|
@ -245,9 +247,9 @@ o.spec("EventQueueTest", function () {
|
|||
|
||||
o("create (list 1) + update (list 1) + delete (list 2) == create (list 1) + delete (list 2)", async function () {
|
||||
// entity updates with for the same element id but different list IDs do not influence each other
|
||||
const createEvent1 = createUpdate(OperationType.CREATE, "list1", "1", "u1")
|
||||
const updateEvent1 = createUpdate(OperationType.UPDATE, "list1", "1", "u2")
|
||||
const deleteEvent1 = createUpdate(OperationType.DELETE, "list2", "1", "u3")
|
||||
const createEvent1 = createUpdate(OperationType.CREATE, "list1", "1")
|
||||
const updateEvent1 = createUpdate(OperationType.UPDATE, "list1", "1")
|
||||
const deleteEvent1 = createUpdate(OperationType.DELETE, "list2", "1")
|
||||
|
||||
queue.add("batch-id-1", "group-id", [createEvent1])
|
||||
queue.add("batch-id-2", "group-id", [updateEvent1])
|
||||
|
@ -255,8 +257,8 @@ o.spec("EventQueueTest", function () {
|
|||
queue.resume()
|
||||
await lastProcess.promise
|
||||
|
||||
const expectedCreate = createUpdate(OperationType.CREATE, createEvent1.instanceListId, createEvent1.instanceId, "u1")
|
||||
const expectedDelete = createUpdate(OperationType.DELETE, deleteEvent1.instanceListId, deleteEvent1.instanceId, "u3")
|
||||
const expectedCreate = createUpdate(OperationType.CREATE, createEvent1.instanceListId, createEvent1.instanceId)
|
||||
const expectedDelete = createUpdate(OperationType.DELETE, deleteEvent1.instanceListId, deleteEvent1.instanceId)
|
||||
|
||||
o(processElement.invocations).deepEquals([
|
||||
[{ events: [expectedCreate], batchId: "batch-id-1", groupId: "group-id" }],
|
||||
|
@ -265,8 +267,8 @@ o.spec("EventQueueTest", function () {
|
|||
})
|
||||
|
||||
o("same batch in two different groups", async function () {
|
||||
const createEvent1 = createUpdate(OperationType.CREATE, "old-mail-list", "1", "u0")
|
||||
const createEvent2 = createUpdate(OperationType.CREATE, "old-mail-list", "1", "u0")
|
||||
const createEvent1 = createUpdate(OperationType.CREATE, "old-mail-list", "1")
|
||||
const createEvent2 = createUpdate(OperationType.CREATE, "old-mail-list", "1")
|
||||
|
||||
queue.add("batch-id-1", "group-id-1", [createEvent1])
|
||||
queue.add("batch-id-1", "group-id-2", [createEvent2])
|
||||
|
@ -282,10 +284,10 @@ o.spec("EventQueueTest", function () {
|
|||
o(
|
||||
"[delete (list 1) + create (list 2)] + delete (list 2) + create (list 2) = [delete (list 1) + create (list 2)] + delete (list 2) + create (list 2)",
|
||||
async function () {
|
||||
const deleteEvent1 = createUpdate(OperationType.DELETE, "l1", "1", "u0")
|
||||
const createEvent1 = createUpdate(OperationType.CREATE, "l2", "1", "u1")
|
||||
const deleteEvent2 = createUpdate(OperationType.DELETE, "l2", "1", "u2")
|
||||
const createEvent2 = createUpdate(OperationType.CREATE, "l2", "1", "u3")
|
||||
const deleteEvent1 = createUpdate(OperationType.DELETE, "l1", "1")
|
||||
const createEvent1 = createUpdate(OperationType.CREATE, "l2", "1")
|
||||
const deleteEvent2 = createUpdate(OperationType.DELETE, "l2", "1")
|
||||
const createEvent2 = createUpdate(OperationType.CREATE, "l2", "1")
|
||||
|
||||
queue.add("batch-id-1", "group-id-1", [deleteEvent1, createEvent1])
|
||||
queue.add("batch-id-2", "group-id-1", [deleteEvent2])
|
||||
|
@ -305,26 +307,21 @@ o.spec("EventQueueTest", function () {
|
|||
const batchId = "batch-id-1"
|
||||
const groupId = "group-id-1"
|
||||
const instanceId = "instance-id-1"
|
||||
const eventId = "event-id-1"
|
||||
const updateEvent1 = createUpdate(OperationType.UPDATE, "", instanceId, eventId)
|
||||
const updateEvent2 = createUpdate(OperationType.UPDATE, "", instanceId, eventId)
|
||||
updateEvent1.typeId = GroupTypeRef.typeId.toString()
|
||||
updateEvent2.typeId = MailboxGroupRootTypeRef.typeId.toString()
|
||||
const updateEvent1 = createUpdate(OperationType.UPDATE, "", instanceId)
|
||||
const updateEvent2 = createUpdate(OperationType.UPDATE, "", instanceId)
|
||||
updateEvent1.typeRef = GroupTypeRef
|
||||
updateEvent2.typeRef = MailboxGroupRootTypeRef
|
||||
queue.add(batchId, groupId, [updateEvent1])
|
||||
queue.add(batchId, groupId, [updateEvent2])
|
||||
})
|
||||
|
||||
function createUpdate(type: OperationType, listId: Id, instanceId: Id, eventId?: Id): EntityUpdate {
|
||||
let update = createTestEntity(EntityUpdateTypeRef)
|
||||
update.operation = type
|
||||
update.instanceListId = listId
|
||||
update.instanceId = instanceId
|
||||
update.typeId = MailTypeRef.typeId.toString()
|
||||
update.application = MailTypeRef.app
|
||||
if (eventId) {
|
||||
update._id = eventId
|
||||
function createUpdate(type: OperationType, listId: Id, instanceId: Id): EntityUpdateData {
|
||||
return {
|
||||
typeRef: MailTypeRef,
|
||||
operation: type,
|
||||
instanceId,
|
||||
instanceListId: listId,
|
||||
}
|
||||
return update
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -337,21 +334,19 @@ o.spec("EventQueueTest", function () {
|
|||
batchMod(
|
||||
batchId,
|
||||
[
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: "tutanota",
|
||||
typeId: MailTypeRef.typeId.toString(),
|
||||
operation: OperationType.CREATE,
|
||||
{
|
||||
typeRef: MailTypeRef,
|
||||
instanceId,
|
||||
instanceListId,
|
||||
}),
|
||||
operation: OperationType.CREATE,
|
||||
},
|
||||
],
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: "tutanota",
|
||||
typeId: MailTypeRef.typeId.toString(),
|
||||
operation: OperationType.CREATE,
|
||||
{
|
||||
typeRef: MailTypeRef,
|
||||
instanceId,
|
||||
instanceListId,
|
||||
}),
|
||||
operation: OperationType.CREATE,
|
||||
},
|
||||
),
|
||||
).equals(EntityModificationType.CREATE)
|
||||
})
|
||||
|
@ -361,28 +356,25 @@ o.spec("EventQueueTest", function () {
|
|||
batchMod(
|
||||
batchId,
|
||||
[
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: "tutanota",
|
||||
typeId: MailTypeRef.typeId.toString(),
|
||||
operation: OperationType.DELETE,
|
||||
{
|
||||
typeRef: MailTypeRef,
|
||||
instanceId: "instanceId2",
|
||||
instanceListId,
|
||||
}),
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: "tutanota",
|
||||
typeId: MailTypeRef.typeId.toString(),
|
||||
operation: OperationType.CREATE,
|
||||
operation: OperationType.DELETE,
|
||||
},
|
||||
{
|
||||
typeRef: MailTypeRef,
|
||||
instanceId,
|
||||
instanceListId,
|
||||
}),
|
||||
operation: OperationType.CREATE,
|
||||
},
|
||||
],
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: "tutanota",
|
||||
typeId: MailTypeRef.typeId.toString(),
|
||||
operation: OperationType.CREATE,
|
||||
{
|
||||
typeRef: MailTypeRef,
|
||||
instanceId,
|
||||
instanceListId,
|
||||
}),
|
||||
operation: OperationType.CREATE,
|
||||
},
|
||||
),
|
||||
).equals(EntityModificationType.CREATE)
|
||||
})
|
||||
|
@ -392,28 +384,25 @@ o.spec("EventQueueTest", function () {
|
|||
batchMod(
|
||||
batchId,
|
||||
[
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: "tutanota",
|
||||
typeId: MailTypeRef.typeId.toString(),
|
||||
operation: OperationType.DELETE,
|
||||
{
|
||||
typeRef: MailTypeRef,
|
||||
instanceId,
|
||||
instanceListId: "instanceListId2",
|
||||
}),
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: "tutanota",
|
||||
typeId: MailTypeRef.typeId.toString(),
|
||||
operation: OperationType.CREATE,
|
||||
operation: OperationType.DELETE,
|
||||
},
|
||||
{
|
||||
typeRef: MailTypeRef,
|
||||
instanceId,
|
||||
instanceListId,
|
||||
}),
|
||||
operation: OperationType.CREATE,
|
||||
},
|
||||
],
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: "tutanota",
|
||||
typeId: MailTypeRef.typeId.toString(),
|
||||
operation: OperationType.CREATE,
|
||||
{
|
||||
typeRef: MailTypeRef,
|
||||
instanceId,
|
||||
instanceListId,
|
||||
}),
|
||||
operation: OperationType.CREATE,
|
||||
},
|
||||
),
|
||||
).equals(EntityModificationType.CREATE)
|
||||
})
|
||||
|
@ -423,28 +412,25 @@ o.spec("EventQueueTest", function () {
|
|||
batchMod(
|
||||
batchId,
|
||||
[
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: "tutanota",
|
||||
typeId: ContactTypeRef.typeId.toString(),
|
||||
{
|
||||
typeRef: ContactTypeRef,
|
||||
instanceId,
|
||||
instanceListId,
|
||||
operation: OperationType.DELETE,
|
||||
},
|
||||
{
|
||||
typeRef: MailTypeRef,
|
||||
instanceId,
|
||||
instanceListId,
|
||||
}),
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: "tutanota",
|
||||
typeId: MailTypeRef.typeId.toString(),
|
||||
operation: OperationType.CREATE,
|
||||
instanceId,
|
||||
instanceListId,
|
||||
}),
|
||||
},
|
||||
],
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: "tutanota",
|
||||
typeId: MailTypeRef.typeId.toString(),
|
||||
operation: OperationType.CREATE,
|
||||
{
|
||||
typeRef: MailTypeRef,
|
||||
instanceId,
|
||||
instanceListId,
|
||||
}),
|
||||
operation: OperationType.CREATE,
|
||||
},
|
||||
),
|
||||
).equals(EntityModificationType.CREATE)
|
||||
})
|
||||
|
@ -454,21 +440,19 @@ o.spec("EventQueueTest", function () {
|
|||
batchMod(
|
||||
batchId,
|
||||
[
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: "tutanota",
|
||||
typeId: MailTypeRef.typeId.toString(),
|
||||
operation: OperationType.CREATE,
|
||||
{
|
||||
typeRef: MailTypeRef,
|
||||
instanceId,
|
||||
instanceListId,
|
||||
}),
|
||||
operation: OperationType.CREATE,
|
||||
},
|
||||
],
|
||||
createTestEntity(EntityUpdateTypeRef, {
|
||||
application: "tutanota",
|
||||
typeId: MailTypeRef.typeId.toString(),
|
||||
operation: OperationType.DELETE,
|
||||
{
|
||||
typeRef: MailTypeRef,
|
||||
instanceId,
|
||||
instanceListId,
|
||||
}),
|
||||
operation: OperationType.DELETE,
|
||||
},
|
||||
),
|
||||
).equals(EntityModificationType.CREATE)
|
||||
})
|
||||
|
|
|
@ -15,13 +15,13 @@ import {
|
|||
} from "../../../../../src/common/api/worker/search/IndexUtils.js"
|
||||
import { base64ToUint8Array, byteLength, concat, utf8Uint8ArrayToString } from "@tutao/tutanota-utils"
|
||||
import type { SearchIndexEntry, SearchIndexMetaDataRow } from "../../../../../src/common/api/worker/search/SearchTypes.js"
|
||||
import { EntityUpdateTypeRef, GroupMembershipTypeRef, UserTypeRef } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { GroupMembershipTypeRef, UserTypeRef } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { ContactTypeRef, MailTypeRef } from "../../../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { GroupType, OperationType } from "../../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import { aes256RandomKey, fixedIv, unauthenticatedAesDecrypt } from "@tutao/tutanota-crypto"
|
||||
import { resolveClientTypeReference } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { createTestEntity } from "../../../TestUtils.js"
|
||||
import { containsEventOfType, entityUpdateToUpdateData, EntityUpdateData } from "../../../../../src/common/api/common/utils/EntityUpdateUtils.js"
|
||||
import { containsEventOfType, EntityUpdateData } from "../../../../../src/common/api/common/utils/EntityUpdateUtils.js"
|
||||
import { ClientModelInfo } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
o.spec("Index Utils", () => {
|
||||
o("encryptIndexKey", function () {
|
||||
|
@ -118,7 +118,7 @@ o.spec("Index Utils", () => {
|
|||
// o(typeRefToTypeInfo(UserTypeRef).appId).equals(0)
|
||||
// o(typeRefToTypeInfo(UserTypeRef).typeId).equals(UserTypeModel.id)
|
||||
o(typeRefToTypeInfo(ContactTypeRef).appId).equals(1)
|
||||
const ContactTypeModel = await resolveClientTypeReference(ContactTypeRef)
|
||||
const ContactTypeModel = await ClientModelInfo.getNewInstanceForTestsOnly().resolveClientTypeReference(ContactTypeRef)
|
||||
o(typeRefToTypeInfo(ContactTypeRef).typeId).equals(ContactTypeModel.id)
|
||||
})
|
||||
o("userIsGlobalAdmin", function () {
|
||||
|
@ -179,10 +179,11 @@ o.spec("Index Utils", () => {
|
|||
})
|
||||
o("containsEventOfType", function () {
|
||||
function createUpdate(type: OperationType, id: Id): EntityUpdateData {
|
||||
let update = createTestEntity(EntityUpdateTypeRef)
|
||||
update.operation = type
|
||||
update.instanceId = id
|
||||
return entityUpdateToUpdateData(update)
|
||||
return {
|
||||
operation: type,
|
||||
instanceId: id,
|
||||
instanceListId: "",
|
||||
} as Partial<EntityUpdateData> as EntityUpdateData
|
||||
}
|
||||
|
||||
o(containsEventOfType([], OperationType.CREATE, "1")).equals(false)
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
getIdFromEncSearchIndexEntry,
|
||||
typeRefToTypeInfo,
|
||||
} from "../../../../../src/common/api/worker/search/IndexUtils.js"
|
||||
import { assertNotNull, base64ToUint8Array, concat, defer, downcast, neverNull, noOp, PromisableWrapper, uint8ArrayToBase64 } from "@tutao/tutanota-utils"
|
||||
import { base64ToUint8Array, concat, defer, downcast, neverNull, noOp, PromisableWrapper, uint8ArrayToBase64 } from "@tutao/tutanota-utils"
|
||||
import { spy } from "@tutao/tutanota-test-utils"
|
||||
import { ContactTypeRef, MailTypeRef } from "../../../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { DbTransaction } from "../../../../../src/common/api/worker/search/DbFacade.js"
|
||||
|
@ -34,10 +34,11 @@ import { IndexerCore } from "../../../../../src/mail-app/workerUtils/index/Index
|
|||
import { elementIdPart, generatedIdToTimestamp, listIdPart, timestampToGeneratedId } from "../../../../../src/common/api/common/utils/EntityUtils.js"
|
||||
import { createTestEntity, makeCore } from "../../../TestUtils.js"
|
||||
import { Aes256Key, aes256RandomKey, aesEncrypt, fixedIv, IV_BYTE_LENGTH, random, unauthenticatedAesDecrypt } from "@tutao/tutanota-crypto"
|
||||
import { resolveClientTypeReference } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { ElementDataOS, GroupDataOS, SearchIndexMetaDataOS, SearchIndexOS } from "../../../../../src/common/api/worker/search/IndexTables.js"
|
||||
import { AttributeModel } from "../../../../../src/common/api/common/AttributeModel"
|
||||
import { ModelValue, TypeModel } from "../../../../../src/common/api/common/EntityTypes"
|
||||
import { ClientModelInfo } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { EntityUpdateData } from "../../../../../src/common/api/common/utils/EntityUpdateUtils"
|
||||
import { OperationType } from "../../../../../src/common/api/common/TutanotaConstants"
|
||||
|
||||
const mailTypeInfo = typeRefToTypeInfo(MailTypeRef)
|
||||
const contactTypeInfo = typeRefToTypeInfo(ContactTypeRef)
|
||||
|
@ -64,7 +65,7 @@ function compareBinaryBlocks(actual: Uint8Array, expected: Uint8Array) {
|
|||
|
||||
o.spec("IndexerCore test", () => {
|
||||
o("createIndexEntriesForAttributes", async function () {
|
||||
const ContactModel = await resolveClientTypeReference(ContactTypeRef)
|
||||
const ContactModel = await ClientModelInfo.getNewInstanceForTestsOnly().resolveClientTypeReference(ContactTypeRef)
|
||||
|
||||
let core = makeCore()
|
||||
let contact = createTestEntity(ContactTypeRef)
|
||||
|
@ -1024,12 +1025,14 @@ o.spec("IndexerCore test", () => {
|
|||
|
||||
const instanceId = "L-dNNLe----1"
|
||||
const instanceIdTimestamp = generatedIdToTimestamp(instanceId)
|
||||
const event = createTestEntity(EntityUpdateTypeRef)
|
||||
event.application = MailTypeRef.app
|
||||
event.typeId = MailTypeRef.typeId.toString()
|
||||
const event: EntityUpdateData = {
|
||||
typeRef: MailTypeRef,
|
||||
instanceId,
|
||||
instanceListId: "",
|
||||
operation: OperationType.CREATE,
|
||||
}
|
||||
const metaRowId = 3
|
||||
const anotherMetaRowId = 4
|
||||
event.instanceId = instanceId
|
||||
const transaction: any = {
|
||||
get: (os, key) => {
|
||||
o(os).equals(ElementDataOS)
|
||||
|
@ -1087,10 +1090,12 @@ o.spec("IndexerCore test", () => {
|
|||
let indexUpdate = _createNewIndexUpdate(mailTypeInfo)
|
||||
|
||||
let instanceId = "123"
|
||||
let event = createTestEntity(EntityUpdateTypeRef)
|
||||
event.instanceId = instanceId
|
||||
event.application = MailTypeRef.app
|
||||
event.typeId = MailTypeRef.typeId.toString()
|
||||
let event: EntityUpdateData = {
|
||||
typeRef: MailTypeRef,
|
||||
instanceId,
|
||||
instanceListId: "",
|
||||
operation: OperationType.CREATE,
|
||||
}
|
||||
let transaction: any = {
|
||||
get: (os, key) => {
|
||||
o(os).equals(ElementDataOS)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -11,7 +11,7 @@ import {
|
|||
} from "../../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import { IndexerCore } from "../../../../../src/mail-app/workerUtils/index/IndexerCore.js"
|
||||
import type { EntityUpdate } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { EntityUpdateTypeRef, GroupMembershipTypeRef, UserTypeRef } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { GroupMembershipTypeRef, UserTypeRef } from "../../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import {
|
||||
_getCurrentIndexTimestamp,
|
||||
INITIAL_MAIL_INDEX_INTERVAL_DAYS,
|
||||
|
@ -59,7 +59,6 @@ import { EntityRestClientMock } from "../rest/EntityRestClientMock.js"
|
|||
import type { DateProvider } from "../../../../../src/common/api/worker/DateProvider.js"
|
||||
import { LocalTimeDateProvider } from "../../../../../src/common/api/worker/DateProvider.js"
|
||||
import { aes256RandomKey, fixedIv } from "@tutao/tutanota-crypto"
|
||||
import { resolveClientTypeReference } from "../../../../../src/common/api/common/EntityFunctions.js"
|
||||
import { matchers, object, verify, when } from "testdouble"
|
||||
import { InfoMessageHandler } from "../../../../../src/common/gui/InfoMessageHandler.js"
|
||||
import { ElementDataOS, GroupDataOS, Metadata as MetaData, MetaDataOS } from "../../../../../src/common/api/worker/search/IndexTables.js"
|
||||
|
@ -70,6 +69,8 @@ import { BulkMailLoader, MAIL_INDEXER_CHUNK, MailWithMailDetails } from "../../.
|
|||
import { DbFacade } from "../../../../../src/common/api/worker/search/DbFacade"
|
||||
import { ProgressMonitor } from "../../../../../src/common/api/common/utils/ProgressMonitor"
|
||||
import { AttributeModel } from "../../../../../src/common/api/common/AttributeModel"
|
||||
import { ClientModelInfo, ClientTypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { EntityUpdateData } from "../../../../../src/common/api/common/utils/EntityUpdateUtils"
|
||||
|
||||
class FixedDateProvider implements DateProvider {
|
||||
now: number
|
||||
|
@ -98,12 +99,16 @@ o.spec("MailIndexer test", () => {
|
|||
let bulkMailLoader: BulkMailLoader
|
||||
let dateProvider: DateProvider
|
||||
let mailFacade: MailFacade
|
||||
let clientTypeModelResolver: ClientTypeModelResolver
|
||||
o.beforeEach(function () {
|
||||
entityMock = new EntityRestClientMock()
|
||||
entityClient = new EntityClient(entityMock)
|
||||
bulkMailLoader = new BulkMailLoader(entityClient, new EntityClient(entityMock))
|
||||
dateProvider = new LocalTimeDateProvider()
|
||||
clientTypeModelResolver = ClientModelInfo.getNewInstanceForTestsOnly()
|
||||
entityClient = new EntityClient(entityMock, clientTypeModelResolver)
|
||||
mailFacade = object()
|
||||
bulkMailLoader = new BulkMailLoader(entityClient, new EntityClient(entityMock, clientTypeModelResolver))
|
||||
dateProvider = new LocalTimeDateProvider()
|
||||
bulkMailLoader = new BulkMailLoader(entityClient, new EntityClient(entityMock, clientTypeModelResolver))
|
||||
dateProvider = new LocalTimeDateProvider()
|
||||
})
|
||||
o("createMailIndexEntries without entries", function () {
|
||||
let mail = createTestEntity(MailTypeRef)
|
||||
|
@ -202,7 +207,7 @@ o.spec("MailIndexer test", () => {
|
|||
value: h.value(),
|
||||
}
|
||||
})
|
||||
const MailModel = await resolveClientTypeReference(MailTypeRef)
|
||||
const MailModel = await clientTypeModelResolver.resolveClientTypeReference(MailTypeRef)
|
||||
o(JSON.stringify(attributes)).equals(
|
||||
JSON.stringify([
|
||||
{
|
||||
|
@ -297,10 +302,10 @@ o.spec("MailIndexer test", () => {
|
|||
await o(() => indexer.processNewMail([event.instanceListId, event.instanceId])).asyncThrows(Error)
|
||||
})
|
||||
o("processMovedMail", async function () {
|
||||
let event: EntityUpdate = {
|
||||
let event: EntityUpdateData = {
|
||||
instanceListId: "new-list-id",
|
||||
instanceId: "eid",
|
||||
} as any
|
||||
} as Partial<EntityUpdateData> as EntityUpdateData
|
||||
let elementData: ElementDataDbRow = ["old-list-id", new Uint8Array(0), "owner-group-id"]
|
||||
let db: Db = {
|
||||
key: aes256RandomKey(),
|
||||
|
@ -980,17 +985,13 @@ o.spec("MailIndexer test", () => {
|
|||
})
|
||||
})
|
||||
|
||||
function createUpdate(type: OperationType, listId: Id, instanceId: Id, eventId?: Id) {
|
||||
let update = createTestEntity(EntityUpdateTypeRef)
|
||||
update.operation = type
|
||||
update.instanceListId = listId
|
||||
update.instanceId = instanceId
|
||||
|
||||
if (eventId) {
|
||||
update._id = eventId
|
||||
function createUpdate(type: OperationType, listId: Id, instanceId: Id): EntityUpdateData {
|
||||
return {
|
||||
typeRef: MailTypeRef,
|
||||
operation: type,
|
||||
instanceListId: listId,
|
||||
instanceId: instanceId,
|
||||
}
|
||||
|
||||
return update
|
||||
}
|
||||
|
||||
async function indexMailboxTest(startTimestamp: number, endIndexTimstamp: number, fullyIndexed: boolean, indexMailList: boolean) {
|
||||
|
@ -1030,7 +1031,7 @@ async function indexMailboxTest(startTimestamp: number, endIndexTimstamp: number
|
|||
iv: fixedIv,
|
||||
} as any
|
||||
const infoMessageHandler = object<InfoMessageHandler>()
|
||||
const entityClient = new EntityClient(entityMock)
|
||||
const entityClient = new EntityClient(entityMock, ClientModelInfo.getNewInstanceForTestsOnly())
|
||||
const bulkMailLoader = new BulkMailLoader(entityClient, entityClient)
|
||||
const indexer = mock(
|
||||
new MailIndexer(core, db, infoMessageHandler, () => bulkMailLoader, entityClient, new LocalTimeDateProvider(), null as any),
|
||||
|
@ -1112,7 +1113,7 @@ function _prepareProcessEntityTests(indexingEnabled: boolean, mailState: MailSta
|
|||
const entityMock = new EntityRestClientMock()
|
||||
entityMock.addBlobInstances(mailDetailsBlob)
|
||||
entityMock.addListInstances(mail)
|
||||
const entityClient = new EntityClient(entityMock)
|
||||
const entityClient = new EntityClient(entityMock, ClientModelInfo.getNewInstanceForTestsOnly())
|
||||
const bulkMailLoader = new BulkMailLoader(entityClient, entityClient)
|
||||
return mock(new MailIndexer(core, db, null as any, () => bulkMailLoader, entityClient, new LocalTimeDateProvider(), mailFacade), (mocked) => {
|
||||
mocked.processNewMail = spy(mocked.processNewMail.bind(mocked))
|
||||
|
|
|
@ -29,6 +29,7 @@ import { aes256RandomKey, fixedIv } from "@tutao/tutanota-crypto"
|
|||
import { ElementDataOS, SearchIndexMetaDataOS, SearchIndexOS } from "../../../../../src/common/api/worker/search/IndexTables.js"
|
||||
import { object, when } from "testdouble"
|
||||
import { EntityClient } from "../../../../../src/common/api/common/EntityClient.js"
|
||||
import { ClientModelInfo } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
type SearchIndexEntryWithType = SearchIndexEntry & {
|
||||
typeInfo: TypeInfo
|
||||
|
@ -69,6 +70,7 @@ o.spec("SearchFacade test", () => {
|
|||
[],
|
||||
browserData,
|
||||
entityClient,
|
||||
ClientModelInfo.getNewInstanceForTestsOnly(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,21 +8,26 @@ import { downcast } from "@tutao/tutanota-utils"
|
|||
import { aes256RandomKey, fixedIv } from "@tutao/tutanota-crypto"
|
||||
import { SearchTermSuggestionsOS } from "../../../../../src/common/api/worker/search/IndexTables.js"
|
||||
import { spy } from "@tutao/tutanota-test-utils"
|
||||
import { resolveClientTypeReference } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { ClientModelInfo, ClientTypeModelResolver } from "../../../../../src/common/api/common/EntityFunctions"
|
||||
import { TypeModel } from "../../../../../src/common/api/common/EntityTypes"
|
||||
import { Db } from "../../../../../src/common/api/worker/search/SearchTypes"
|
||||
import { DbFacade } from "../../../../../src/common/api/worker/search/DbFacade"
|
||||
|
||||
o.spec("SuggestionFacade test", () => {
|
||||
let db
|
||||
let facade
|
||||
let contactTypeModel
|
||||
let db: Db
|
||||
let facade: SuggestionFacade<any>
|
||||
let contactTypeModel: TypeModel
|
||||
let clientModelResolver: ClientTypeModelResolver
|
||||
o.beforeEach(async function () {
|
||||
db = {
|
||||
key: aes256RandomKey(),
|
||||
iv: fixedIv,
|
||||
dbFacade: {},
|
||||
dbFacade: {} as unknown as DbFacade,
|
||||
initialized: Promise.resolve(),
|
||||
}
|
||||
facade = new SuggestionFacade(ContactTypeRef, db)
|
||||
contactTypeModel = await resolveClientTypeReference(ContactTypeRef)
|
||||
clientModelResolver = ClientModelInfo.getNewInstanceForTestsOnly()
|
||||
facade = new SuggestionFacade(ContactTypeRef, db, clientModelResolver)
|
||||
contactTypeModel = await clientModelResolver.resolveClientTypeReference(ContactTypeRef)
|
||||
})
|
||||
o("add and get suggestion", () => {
|
||||
o(facade.getSuggestions("a").join("")).equals("")
|
||||
|
|
|
@ -38,6 +38,7 @@ import { incrementByRepeatPeriod } from "../../../src/common/calendar/date/Calen
|
|||
import { ExternalCalendarFacade } from "../../../src/common/native/common/generatedipc/ExternalCalendarFacade.js"
|
||||
import { DeviceConfig } from "../../../src/common/misc/DeviceConfig.js"
|
||||
import { SyncTracker } from "../../../src/common/api/main/SyncTracker.js"
|
||||
import { ClientModelInfo } from "../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
o.spec("CalendarModel", function () {
|
||||
o.spec("incrementByRepeatPeriod", function () {
|
||||
|
@ -673,12 +674,10 @@ o.spec("CalendarModel", function () {
|
|||
|
||||
// calendar update create event
|
||||
await eventControllerMock.sendEvent({
|
||||
application: CalendarEventUpdateTypeRef.app,
|
||||
typeId: CalendarEventUpdateTypeRef.typeId,
|
||||
typeRef: CalendarEventUpdateTypeRef,
|
||||
instanceListId: listIdPart(eventUpdate._id),
|
||||
instanceId: elementIdPart(eventUpdate._id),
|
||||
operation: OperationType.CREATE,
|
||||
type: "CalendarEventUpdate",
|
||||
})
|
||||
|
||||
o(model.getFileIdToSkippedCalendarEventUpdates().get(getElementId(calendarFile))!).deepEquals(eventUpdate)
|
||||
|
@ -690,12 +689,10 @@ o.spec("CalendarModel", function () {
|
|||
// set owner enc session key to ensure that we can process the calendar event file
|
||||
calendarFile._ownerEncSessionKey = hexToUint8Array("01")
|
||||
await eventControllerMock.sendEvent({
|
||||
application: FileTypeRef.app,
|
||||
typeId: FileTypeRef.typeId,
|
||||
typeRef: FileTypeRef,
|
||||
instanceListId: listIdPart(calendarFile._id),
|
||||
instanceId: elementIdPart(calendarFile._id),
|
||||
operation: OperationType.UPDATE,
|
||||
type: "File",
|
||||
})
|
||||
|
||||
o(model.getFileIdToSkippedCalendarEventUpdates().size).deepEquals(0)
|
||||
|
@ -806,7 +803,7 @@ function init({
|
|||
restClientMock,
|
||||
loginController = makeLoginController(),
|
||||
progressTracker = makeProgressTracker(),
|
||||
entityClient = new EntityClient(restClientMock),
|
||||
entityClient = new EntityClient(restClientMock, ClientModelInfo.getNewInstanceForTestsOnly()),
|
||||
mailModel = makeMailModel(),
|
||||
alarmScheduler = makeAlarmScheduler(),
|
||||
calendarFacade = makeCalendarFacade(
|
||||
|
|
|
@ -30,6 +30,7 @@ import { addDaysForEventInstance, getMonthRange } from "../../../src/common/cale
|
|||
import { CalendarEventModel, CalendarOperation, EventSaveResult } from "../../../src/calendar-app/calendar/gui/eventeditor-model/CalendarEventModel.js"
|
||||
import { ContactModel } from "../../../src/common/contactsFunctionality/ContactModel.js"
|
||||
import { CalendarEventTypeRef } from "../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { ClientModelInfo } from "../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
let saveAndSendMock
|
||||
let rescheduleEventMock
|
||||
|
@ -87,7 +88,7 @@ o.spec("CalendarViewModel", function () {
|
|||
contactPreviewModelFactory,
|
||||
calendarModel,
|
||||
eventsRepository,
|
||||
new EntityClient(entityClientMock),
|
||||
new EntityClient(entityClientMock, ClientModelInfo.getNewInstanceForTestsOnly()),
|
||||
eventController,
|
||||
progressTracker,
|
||||
deviceConfig,
|
||||
|
@ -413,12 +414,10 @@ o.spec("CalendarViewModel", function () {
|
|||
o(viewModel.temporaryEvents.some((e) => e.uid === eventToDrag.uid)).equals(true)("Has transient event")
|
||||
o(entityListeners.length).equals(1)("Listener was added")
|
||||
const entityUpdate: EntityUpdateData = {
|
||||
application: "tutanota",
|
||||
typeId: CalendarEventTypeRef.typeId,
|
||||
typeRef: CalendarEventTypeRef,
|
||||
instanceListId: getListId(eventToDrag),
|
||||
instanceId: getElementId(eventToDrag),
|
||||
operation: OperationType.CREATE,
|
||||
type: "CalendarEvent",
|
||||
}
|
||||
const updatedEventFromServer = makeEvent(getElementId(eventToDrag), newData, new Date(2021, 0, 5, 14, 30), assertNotNull(eventToDrag.uid))
|
||||
entityClientMock.addListInstances(updatedEventFromServer)
|
||||
|
|
|
@ -4,11 +4,10 @@ import { DesktopAlarmStorage } from "../../../../src/common/desktop/sse/DesktopA
|
|||
import { DesktopConfig } from "../../../../src/common/desktop/config/DesktopConfig.js"
|
||||
import { DesktopNativeCryptoFacade } from "../../../../src/common/desktop/DesktopNativeCryptoFacade.js"
|
||||
import type { DesktopKeyStoreFacade } from "../../../../src/common/desktop/DesktopKeyStoreFacade.js"
|
||||
import { createTestEntity, makeKeyStoreFacade } from "../../TestUtils.js"
|
||||
import { clientInitializedTypeModelResolver, instancePipelineFromTypeModelResolver, makeKeyStoreFacade, createTestEntity } from "../../TestUtils.js"
|
||||
import { DesktopConfigKey } from "../../../../src/common/desktop/config/ConfigKeys.js"
|
||||
import { assertNotNull, uint8ArrayToBase64 } from "@tutao/tutanota-utils"
|
||||
import { InstancePipeline } from "../../../../src/common/api/worker/crypto/InstancePipeline"
|
||||
import { resolveClientTypeReference, resolveServerTypeReference } from "../../../../src/common/api/common/EntityFunctions"
|
||||
import { aes256RandomKey, bitArrayToUint8Array, encryptKey, uint8ArrayToBitArray } from "@tutao/tutanota-crypto"
|
||||
import {
|
||||
AlarmInfoTypeRef,
|
||||
|
@ -17,10 +16,14 @@ import {
|
|||
NotificationSessionKeyTypeRef,
|
||||
} from "../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { hasError } from "../../../../src/common/api/common/utils/ErrorUtils.js"
|
||||
import { TypeModelResolver } from "../../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
o.spec("DesktopAlarmStorageTest", function () {
|
||||
let cryptoMock: DesktopNativeCryptoFacade
|
||||
let confMock: DesktopConfig
|
||||
let typeModelResolver: TypeModelResolver
|
||||
let instancePipeline: InstancePipeline
|
||||
let desktopStorage: DesktopAlarmStorage
|
||||
|
||||
const key1 = new Uint8Array([1])
|
||||
const key2 = new Uint8Array([2])
|
||||
|
@ -28,7 +31,6 @@ o.spec("DesktopAlarmStorageTest", function () {
|
|||
const key4 = new Uint8Array([4])
|
||||
const decryptedKey = new Uint8Array([0, 1])
|
||||
const encryptedKey = new Uint8Array([1, 0])
|
||||
const instancePipeline = new InstancePipeline(resolveClientTypeReference, resolveServerTypeReference)
|
||||
|
||||
o.beforeEach(function () {
|
||||
cryptoMock = instance(DesktopNativeCryptoFacade)
|
||||
|
@ -42,12 +44,14 @@ o.spec("DesktopAlarmStorageTest", function () {
|
|||
twoId: uint8ArrayToBase64(key3),
|
||||
fourId: uint8ArrayToBase64(key4),
|
||||
})
|
||||
|
||||
typeModelResolver = clientInitializedTypeModelResolver()
|
||||
instancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
|
||||
const keyStoreFacade: DesktopKeyStoreFacade = makeKeyStoreFacade(new Uint8Array([1, 2, 3]))
|
||||
desktopStorage = new DesktopAlarmStorage(confMock, cryptoMock, keyStoreFacade, instancePipeline, typeModelResolver)
|
||||
})
|
||||
|
||||
o("getPushIdentifierSessionKey with uncached sessionKey", async function () {
|
||||
const keyStoreFacade: DesktopKeyStoreFacade = makeKeyStoreFacade(new Uint8Array([1, 2, 3]))
|
||||
const desktopStorage = new DesktopAlarmStorage(confMock, cryptoMock, keyStoreFacade, instancePipeline)
|
||||
|
||||
const pushIdentifier: IdTuple = ["oneId", "twoId"]
|
||||
const key = await desktopStorage.getPushIdentifierSessionKey(pushIdentifier)
|
||||
|
||||
|
@ -56,9 +60,7 @@ o.spec("DesktopAlarmStorageTest", function () {
|
|||
})
|
||||
|
||||
o("getPushIdentifierSessionKey with cached sessionKey", async function () {
|
||||
const keyStoreFacade: DesktopKeyStoreFacade = makeKeyStoreFacade(new Uint8Array([1, 2, 3]))
|
||||
when(confMock.getVar(matchers.anything())).thenResolve(null)
|
||||
const desktopStorage = new DesktopAlarmStorage(confMock, cryptoMock, keyStoreFacade, instancePipeline)
|
||||
await desktopStorage.storePushIdentifierSessionKey("fourId", key4)
|
||||
|
||||
verify(confMock.setVar(DesktopConfigKey.pushEncSessionKeys, { fourId: uint8ArrayToBase64(encryptedKey) }), { times: 1 })
|
||||
|
@ -69,8 +71,6 @@ o.spec("DesktopAlarmStorageTest", function () {
|
|||
})
|
||||
|
||||
o("getPushIdentifierSessionKey when sessionKey is unavailable", async function () {
|
||||
const keyStoreFacade: DesktopKeyStoreFacade = makeKeyStoreFacade(new Uint8Array([1, 2, 3]))
|
||||
const desktopStorage = new DesktopAlarmStorage(confMock, cryptoMock, keyStoreFacade, instancePipeline)
|
||||
const pushIdentifier: IdTuple = ["fiveId", "sixId"]
|
||||
const key1 = await desktopStorage.getPushIdentifierSessionKey(pushIdentifier)
|
||||
o(key1).equals(null)
|
||||
|
@ -84,7 +84,7 @@ o.spec("DesktopAlarmStorageTest", function () {
|
|||
const pushSessionKey = aes256RandomKey()
|
||||
const pushIdentifierSessionEncSessionKey = encryptKey(pushSessionKey, notificationSessionKey)
|
||||
|
||||
const desktopStorage: DesktopAlarmStorage = new DesktopAlarmStorage(confMock, cryptoMock, keyStoreFacade, instancePipeline)
|
||||
const desktopStorage: DesktopAlarmStorage = new DesktopAlarmStorage(confMock, cryptoMock, keyStoreFacade, instancePipeline, typeModelResolver)
|
||||
await desktopStorage.storePushIdentifierSessionKey("fourId", bitArrayToUint8Array(pushSessionKey))
|
||||
const pushIdentifier: IdTuple = ["threeId", "fourId"]
|
||||
const pushIdentifierSessionKey = await desktopStorage.getPushIdentifierSessionKey(pushIdentifier)
|
||||
|
|
|
@ -11,25 +11,20 @@ import { func, matchers, object, verify, when } from "testdouble"
|
|||
import { CredentialEncryptionMode } from "../../../../src/common/misc/credentials/CredentialEncryptionMode.js"
|
||||
import { ExtendedNotificationMode } from "../../../../src/common/native/common/generatedipc/ExtendedNotificationMode.js"
|
||||
import { createIdTupleWrapper, createNotificationInfo } from "../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { createTestEntity, mockFetchRequest } from "../../TestUtils.js"
|
||||
import { clientInitializedTypeModelResolver, createTestEntity, instancePipelineFromTypeModelResolver, mockFetchRequest } from "../../TestUtils.js"
|
||||
import tutanotaModelInfo from "../../../../src/common/api/entities/tutanota/ModelInfo.js"
|
||||
import { UnencryptedCredentials } from "../../../../src/common/native/common/generatedipc/UnencryptedCredentials.js"
|
||||
import { CredentialType } from "../../../../src/common/misc/credentials/CredentialType.js"
|
||||
import { Mail, MailAddressTypeRef, MailTypeRef } from "../../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { EncryptedAlarmNotification } from "../../../../src/common/native/common/EncryptedAlarmNotification.js"
|
||||
import { OperationType } from "../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import { ApplicationWindow } from "../../../../src/common/desktop/ApplicationWindow.js"
|
||||
import { SseInfo } from "../../../../src/common/desktop/sse/SseInfo.js"
|
||||
import { SseStorage } from "../../../../src/common/desktop/sse/SseStorage.js"
|
||||
import { createSystemMail } from "../../api/common/mail/CommonMailUtilsTest"
|
||||
import { InstancePipeline } from "../../../../src/common/api/worker/crypto/InstancePipeline"
|
||||
import { resolveClientTypeReference, resolveServerTypeReference } from "../../../../src/common/api/common/EntityFunctions"
|
||||
import { aes256RandomKey } from "@tutao/tutanota-crypto"
|
||||
|
||||
type UndiciFetch = typeof undiciFetch
|
||||
|
||||
const nativeInstancePipeline = new InstancePipeline(resolveClientTypeReference, resolveClientTypeReference)
|
||||
|
||||
o.spec("TutaNotificationHandler", () => {
|
||||
let wm: WindowManager
|
||||
let nativeCredentialsFacade: NativeCredentialsFacade
|
||||
|
@ -41,6 +36,7 @@ o.spec("TutaNotificationHandler", () => {
|
|||
let fetch: UndiciFetch
|
||||
let appVersion = "V_1"
|
||||
let handler: TutaNotificationHandler
|
||||
let nativeInstancePipeline: InstancePipeline
|
||||
|
||||
o.beforeEach(() => {
|
||||
wm = object()
|
||||
|
@ -52,6 +48,8 @@ o.spec("TutaNotificationHandler", () => {
|
|||
lang = object()
|
||||
fetch = func<UndiciFetch>()
|
||||
when(lang.get(matchers.anything())).thenDo((arg) => `translated:${arg}`)
|
||||
const typeModelResolver = clientInitializedTypeModelResolver()
|
||||
nativeInstancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
|
||||
handler = new TutaNotificationHandler(
|
||||
wm,
|
||||
nativeCredentialsFacade,
|
||||
|
@ -63,6 +61,7 @@ o.spec("TutaNotificationHandler", () => {
|
|||
fetch,
|
||||
appVersion,
|
||||
nativeInstancePipeline,
|
||||
typeModelResolver,
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -23,10 +23,16 @@ import {
|
|||
NotificationSessionKeyTypeRef,
|
||||
SseConnectDataTypeRef,
|
||||
} from "../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { createTestEntity, mockFetchRequest, removeAggregateIds, removeFinalIvs } from "../../TestUtils.js"
|
||||
import {
|
||||
clientInitializedTypeModelResolver,
|
||||
createTestEntity,
|
||||
instancePipelineFromTypeModelResolver,
|
||||
mockFetchRequest,
|
||||
removeAggregateIds,
|
||||
removeFinalIvs,
|
||||
} from "../../TestUtils.js"
|
||||
import { SseInfo } from "../../../../src/common/desktop/sse/SseInfo.js"
|
||||
import { OperationType } from "../../../../src/common/api/common/TutanotaConstants"
|
||||
import { resolveClientTypeReference } from "../../../../src/common/api/common/EntityFunctions"
|
||||
import { InstancePipeline } from "../../../../src/common/api/worker/crypto/InstancePipeline"
|
||||
import { aes256RandomKey } from "@tutao/tutanota-crypto"
|
||||
import { StrippedEntity } from "../../../../src/common/api/common/utils/EntityUtils"
|
||||
|
@ -37,6 +43,7 @@ import { EncryptedMissedNotification } from "../../../../src/common/native/commo
|
|||
import { assertThrows } from "@tutao/tutanota-test-utils"
|
||||
import { CryptoError } from "@tutao/tutanota-crypto/error.js"
|
||||
import { AttributeModel } from "../../../../src/common/api/common/AttributeModel"
|
||||
import { TypeModelResolver } from "../../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
const APP_V = env.versionNumber
|
||||
|
||||
|
@ -52,6 +59,8 @@ o.spec("TutaSseFacade", () => {
|
|||
let fetch: typeof undiciFetch
|
||||
let date: DateProvider
|
||||
let nativeInstancePipeline: InstancePipeline
|
||||
let typeModelResolver: TypeModelResolver
|
||||
|
||||
o.beforeEach(() => {
|
||||
sseStorage = object()
|
||||
notificationHandler = object()
|
||||
|
@ -60,8 +69,20 @@ o.spec("TutaSseFacade", () => {
|
|||
alarmScheduler = object()
|
||||
fetch = func<typeof undiciFetch>()
|
||||
date = object()
|
||||
nativeInstancePipeline = new InstancePipeline(resolveClientTypeReference, resolveClientTypeReference)
|
||||
sseFacade = new TutaSseFacade(sseStorage, notificationHandler, sseClient, alarmStorage, alarmScheduler, APP_V, fetch, date, nativeInstancePipeline)
|
||||
typeModelResolver = clientInitializedTypeModelResolver()
|
||||
nativeInstancePipeline = instancePipelineFromTypeModelResolver(typeModelResolver)
|
||||
sseFacade = new TutaSseFacade(
|
||||
sseStorage,
|
||||
notificationHandler,
|
||||
sseClient,
|
||||
alarmStorage,
|
||||
alarmScheduler,
|
||||
APP_V,
|
||||
fetch,
|
||||
date,
|
||||
nativeInstancePipeline,
|
||||
typeModelResolver,
|
||||
)
|
||||
})
|
||||
|
||||
function setupSseInfo(template: Partial<SseInfo> = {}): SseInfo {
|
||||
|
@ -254,7 +275,7 @@ o.spec("TutaSseFacade", () => {
|
|||
missedNotification,
|
||||
sk,
|
||||
)) as unknown as ServerModelUntypedInstance
|
||||
const encryptedMissedNotification = await EncryptedMissedNotification.from(untypedInstance)
|
||||
const encryptedMissedNotification = await EncryptedMissedNotification.from(untypedInstance, typeModelResolver)
|
||||
await sseFacade.handleAlarmNotification(encryptedMissedNotification)
|
||||
verify(alarmScheduler.handleDeleteAlarm("alarmId"))
|
||||
})
|
||||
|
@ -291,7 +312,7 @@ o.spec("TutaSseFacade", () => {
|
|||
missedNotification,
|
||||
sk,
|
||||
)) as unknown as ServerModelUntypedInstance
|
||||
const encryptedMissedNotification = await EncryptedMissedNotification.from(untypedInstance)
|
||||
const encryptedMissedNotification = await EncryptedMissedNotification.from(untypedInstance, typeModelResolver)
|
||||
|
||||
await assertThrows(CryptoError, () => sseFacade.handleAlarmNotification(encryptedMissedNotification))
|
||||
verify(alarmStorage.getNotificationSessionKey(anything()))
|
||||
|
@ -339,12 +360,12 @@ o.spec("TutaSseFacade", () => {
|
|||
missedNotification,
|
||||
sk,
|
||||
)) as unknown as ServerModelUntypedInstance
|
||||
const missedNotificationTypeModel = await resolveClientTypeReference(MissedNotificationTypeRef)
|
||||
const alarmNotificationTypeModel = await resolveClientTypeReference(AlarmNotificationTypeRef)
|
||||
const missedNotificationTypeModel = await typeModelResolver.resolveClientTypeReference(MissedNotificationTypeRef)
|
||||
const alarmNotificationTypeModel = await typeModelResolver.resolveClientTypeReference(AlarmNotificationTypeRef)
|
||||
const anAttrId = assertNotNull(AttributeModel.getAttributeId(missedNotificationTypeModel, "alarmNotifications"))
|
||||
const eventStartAttrId = assertNotNull(AttributeModel.getAttributeId(alarmNotificationTypeModel, "eventStart"))
|
||||
downcast<Array<UntypedInstance>>(untypedInstance[anAttrId])[0][eventStartAttrId] = stringToBase64("newDate")
|
||||
const encryptedMissedNotification = await EncryptedMissedNotification.from(untypedInstance)
|
||||
const encryptedMissedNotification = await EncryptedMissedNotification.from(untypedInstance, typeModelResolver)
|
||||
|
||||
await assertThrows(CryptoError, () => sseFacade.handleAlarmNotification(encryptedMissedNotification))
|
||||
verify(alarmStorage.removePushIdentifierKey(anything()))
|
||||
|
|
|
@ -14,7 +14,7 @@ import { ConnectionError } from "../../../src/common/api/common/error/RestError.
|
|||
import { assertThrows } from "@tutao/tutanota-test-utils"
|
||||
import { Mode } from "../../../src/common/api/common/Env.js"
|
||||
import Stream from "mithril/stream"
|
||||
import { createTestEntity } from "../TestUtils.js"
|
||||
import { createTestEntity, withOverriddenEnv } from "../TestUtils.js"
|
||||
|
||||
const { anything, argThat } = matchers
|
||||
const guiDownload = async function (somePromise: Promise<void>, progress?: Stream<number>) {
|
||||
|
@ -29,27 +29,26 @@ o.spec("FileControllerTest", function () {
|
|||
})
|
||||
|
||||
o.spec("native", function () {
|
||||
const androidEnv: Partial<typeof env> = { mode: Mode.App, platformId: "android" }
|
||||
let fileAppMock: NativeFileApp
|
||||
let fileController: FileControllerNative
|
||||
let oldEnv: typeof env
|
||||
|
||||
o.beforeEach(function () {
|
||||
fileAppMock = object()
|
||||
fileController = new FileControllerNative(blobFacadeMock, guiDownload, fileAppMock)
|
||||
oldEnv = globalThis.env
|
||||
globalThis.env = { mode: Mode.App, platformId: "android" } as typeof env
|
||||
})
|
||||
|
||||
o.afterEach(function () {
|
||||
globalThis.env = oldEnv
|
||||
})
|
||||
|
||||
o("should download non-legacy file natively using the blob service", async function () {
|
||||
const blobs = [createTestEntity(BlobTypeRef)]
|
||||
const file = createTestEntity(FileTypeRef, { blobs: blobs, name: "test.txt", mimeType: "plain/text", _id: ["fileListId", "fileElementId"] })
|
||||
const file = createTestEntity(FileTypeRef, {
|
||||
blobs: blobs,
|
||||
name: "test.txt",
|
||||
mimeType: "plain/text",
|
||||
_id: ["fileListId", "fileElementId"],
|
||||
})
|
||||
const fileReference = object<FileReference>()
|
||||
when(blobFacadeMock.downloadAndDecryptNative(anything(), anything(), anything(), anything())).thenResolve(fileReference)
|
||||
const result = await fileController.downloadAndDecrypt(file)
|
||||
const result = await withOverriddenEnv(androidEnv, () => fileController.downloadAndDecrypt(file))
|
||||
verify(
|
||||
blobFacadeMock.downloadAndDecryptNative(
|
||||
ArchiveDataType.Attachments,
|
||||
|
@ -67,9 +66,14 @@ o.spec("FileControllerTest", function () {
|
|||
o("immediately no connection", async function () {
|
||||
const testableFileController = new FileControllerNative(blobFacadeMock, guiDownload, fileAppMock)
|
||||
const blobs = [createTestEntity(BlobTypeRef)]
|
||||
const file = createTestEntity(FileTypeRef, { blobs: blobs, name: "test.txt", mimeType: "plain/text", _id: ["fileListId", "fileElementId"] })
|
||||
const file = createTestEntity(FileTypeRef, {
|
||||
blobs: blobs,
|
||||
name: "test.txt",
|
||||
mimeType: "plain/text",
|
||||
_id: ["fileListId", "fileElementId"],
|
||||
})
|
||||
when(blobFacadeMock.downloadAndDecryptNative(anything(), anything(), anything(), anything())).thenReject(new ConnectionError("no connection"))
|
||||
await assertThrows(ConnectionError, async () => await testableFileController.download(file))
|
||||
await assertThrows(ConnectionError, async () => await withOverriddenEnv(androidEnv, () => testableFileController.download(file)))
|
||||
verify(fileAppMock.deleteFile(anything()), { times: 0 }) // mock for cleanup
|
||||
})
|
||||
o("connection lost after 1 already downloaded attachment- already downloaded attachments are processed", async function () {
|
||||
|
@ -96,7 +100,10 @@ o.spec("FileControllerTest", function () {
|
|||
}
|
||||
when(blobFacadeMock.downloadAndDecryptNative(anything(), anything(), "works.txt", anything())).thenResolve(fileReferenceWorks)
|
||||
when(blobFacadeMock.downloadAndDecryptNative(anything(), anything(), "broken.txt", anything())).thenReject(new ConnectionError("no connection"))
|
||||
await assertThrows(ConnectionError, async () => await testableFileController.downloadAll([fileWorks, fileNotWorks]))
|
||||
await assertThrows(
|
||||
ConnectionError,
|
||||
async () => await withOverriddenEnv(androidEnv, () => testableFileController.downloadAll([fileWorks, fileNotWorks])),
|
||||
)
|
||||
verify(fileAppMock.deleteFile(anything()), { times: 1 }) // mock for cleanup
|
||||
})
|
||||
})
|
||||
|
@ -111,7 +118,12 @@ o.spec("FileControllerTest", function () {
|
|||
|
||||
o("should download non-legacy file non-natively using the blob service", async function () {
|
||||
const blobs = [createTestEntity(BlobTypeRef)]
|
||||
const file = createTestEntity(FileTypeRef, { blobs: blobs, name: "test.txt", mimeType: "plain/text", _id: ["fileListId", "fileElementId"] })
|
||||
const file = createTestEntity(FileTypeRef, {
|
||||
blobs: blobs,
|
||||
name: "test.txt",
|
||||
mimeType: "plain/text",
|
||||
_id: ["fileListId", "fileElementId"],
|
||||
})
|
||||
const data = new Uint8Array([1, 2, 3])
|
||||
when(blobFacadeMock.downloadAndDecrypt(anything(), anything())).thenResolve(data)
|
||||
const result = await fileController.downloadAndDecrypt(file)
|
||||
|
|
|
@ -64,7 +64,6 @@ o.spec("InboxRuleHandlerTest", function () {
|
|||
})
|
||||
o.spec("Test _findMatchingRule", function () {
|
||||
const restClient: EntityRestClientMock = new EntityRestClientMock()
|
||||
const entityClient = new EntityClient(restClient)
|
||||
o("check FROM_EQUALS is applied to from", async function () {
|
||||
const rules: InboxRule[] = [_createRule("sender@tuta.com", InboxRuleType.FROM_EQUALS, ["ruleTarget", "ruleTarget"])]
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import { getElementId, getListId } from "../../../src/common/api/common/utils/En
|
|||
import { MailModel } from "../../../src/mail-app/mail/model/MailModel.js"
|
||||
import { EventController } from "../../../src/common/api/main/EventController.js"
|
||||
import { MailFacade } from "../../../src/common/api/worker/facades/lazy/MailFacade.js"
|
||||
import { ClientModelInfo } from "../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
o.spec("MailModelTest", function () {
|
||||
let notifications: Partial<Notifications>
|
||||
|
@ -44,7 +45,16 @@ o.spec("MailModelTest", function () {
|
|||
when(logins.getUserController()).thenReturn(userController)
|
||||
|
||||
inboxRuleHandler = object()
|
||||
model = new MailModel(downcast({}), mailboxModel, eventController, new EntityClient(restClient), logins, mailFacade, null, null)
|
||||
model = new MailModel(
|
||||
downcast({}),
|
||||
mailboxModel,
|
||||
eventController,
|
||||
new EntityClient(restClient, ClientModelInfo.getNewInstanceForTestsOnly()),
|
||||
logins,
|
||||
mailFacade,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
// not pretty, but works
|
||||
// model.mailboxDetails(mailboxDetails as MailboxDetail[])
|
||||
})
|
||||
|
@ -86,16 +96,12 @@ o.spec("MailModelTest", function () {
|
|||
verify(mailFacade.markMails([mailId1, mailId2, mailId3], true))
|
||||
})
|
||||
|
||||
function makeUpdate(arg: { instanceListId: string; instanceId: Id; operation: OperationType }): EntityUpdateData {
|
||||
return Object.assign(
|
||||
{},
|
||||
{
|
||||
typeId: MailTypeRef.typeId,
|
||||
application: MailTypeRef.app,
|
||||
instanceId: "instanceId",
|
||||
type: "Mail",
|
||||
},
|
||||
arg,
|
||||
)
|
||||
function makeUpdate({ instanceId, instanceListId, operation }: { instanceListId: string; instanceId: Id; operation: OperationType }): EntityUpdateData {
|
||||
return {
|
||||
typeRef: MailTypeRef,
|
||||
operation,
|
||||
instanceListId,
|
||||
instanceId,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -49,7 +49,6 @@ import { MailboxDetail, MailboxModel } from "../../../src/common/mailFunctionali
|
|||
import { SendMailModel, TOO_MANY_VISIBLE_RECIPIENTS } from "../../../src/common/mailFunctionality/SendMailModel.js"
|
||||
import { RecipientField } from "../../../src/common/mailFunctionality/SharedMailUtils.js"
|
||||
import { getContactDisplayName } from "../../../src/common/contactsFunctionality/ContactUtils.js"
|
||||
import { PartialRecipient } from "../../../src/common/api/common/recipients/Recipient"
|
||||
|
||||
const { anything, argThat } = matchers
|
||||
|
||||
|
@ -571,17 +570,46 @@ o.spec("SendMailModel", function () {
|
|||
})
|
||||
|
||||
o("nonmatching event", async function () {
|
||||
await model.handleEntityEvent(downcast(CustomerAccountCreateDataTypeRef))
|
||||
await model.handleEntityEvent(downcast(UserTypeRef))
|
||||
await model.handleEntityEvent(downcast(CustomerTypeRef))
|
||||
await model.handleEntityEvent(downcast(NotificationMailTypeRef))
|
||||
await model.handleEntityEvent(downcast(ChallengeTypeRef))
|
||||
await model.handleEntityEvent(downcast(MailTypeRef))
|
||||
await model.handleEntityEvent({
|
||||
typeRef: CustomerAccountCreateDataTypeRef,
|
||||
operation: OperationType.CREATE,
|
||||
instanceListId: "",
|
||||
instanceId: "",
|
||||
})
|
||||
await model.handleEntityEvent({
|
||||
typeRef: UserTypeRef,
|
||||
operation: OperationType.CREATE,
|
||||
instanceListId: "",
|
||||
instanceId: "",
|
||||
})
|
||||
await model.handleEntityEvent({
|
||||
typeRef: CustomerTypeRef,
|
||||
operation: OperationType.CREATE,
|
||||
instanceListId: "",
|
||||
instanceId: "",
|
||||
})
|
||||
await model.handleEntityEvent({
|
||||
typeRef: NotificationMailTypeRef,
|
||||
operation: OperationType.CREATE,
|
||||
instanceListId: "",
|
||||
instanceId: "",
|
||||
})
|
||||
await model.handleEntityEvent({
|
||||
typeRef: ChallengeTypeRef,
|
||||
operation: OperationType.CREATE,
|
||||
instanceListId: "",
|
||||
instanceId: "",
|
||||
})
|
||||
await model.handleEntityEvent({
|
||||
typeRef: MailTypeRef,
|
||||
operation: OperationType.CREATE,
|
||||
instanceListId: "",
|
||||
instanceId: "",
|
||||
})
|
||||
verify(entity.load(anything(), anything(), anything()), { times: 0 })
|
||||
})
|
||||
|
||||
o("contact updated email kept", async function () {
|
||||
const { app, typeId } = ContactTypeRef
|
||||
const [instanceListId, instanceId] = existingContact._id
|
||||
const contactForUpdate = {
|
||||
firstName: "newfirstname",
|
||||
|
@ -603,19 +631,16 @@ o.spec("SendMailModel", function () {
|
|||
).thenResolve(createContact(Object.assign({ _id: existingContact._id } as Contact, contactForUpdate)))
|
||||
await model.initWithTemplate({ to: recipients }, "somb", "", [], true, "a@b.c", false)
|
||||
await model.handleEntityEvent({
|
||||
application: app,
|
||||
typeId: typeId,
|
||||
typeRef: ContactTypeRef,
|
||||
operation: OperationType.UPDATE,
|
||||
instanceListId,
|
||||
instanceId,
|
||||
type: "Contact",
|
||||
})
|
||||
o(model.allRecipients().length).equals(2)
|
||||
const updatedRecipient = model.allRecipients().find((r) => r.contact && isSameId(r.contact._id, existingContact._id))
|
||||
o(updatedRecipient && updatedRecipient.name).equals(getContactDisplayName(downcast(contactForUpdate)))
|
||||
})
|
||||
o("contact updated email removed or changed", async function () {
|
||||
const { app, typeId } = ContactTypeRef
|
||||
const [instanceListId, instanceId] = existingContact._id
|
||||
const contactForUpdate = {
|
||||
firstName: "james",
|
||||
|
@ -639,28 +664,23 @@ o.spec("SendMailModel", function () {
|
|||
)
|
||||
await model.initWithTemplate({ to: recipients }, "b", "c", [], true, "", false)
|
||||
await model.handleEntityEvent({
|
||||
application: app,
|
||||
typeId: typeId,
|
||||
typeRef: ContactTypeRef,
|
||||
operation: OperationType.UPDATE,
|
||||
instanceListId,
|
||||
instanceId,
|
||||
type: "Contact",
|
||||
})
|
||||
o(model.allRecipients().length).equals(1)
|
||||
const updatedContact = model.allRecipients().find((r) => r.contact && isSameId(r.contact._id, existingContact._id))
|
||||
o(updatedContact ?? null).equals(null)
|
||||
})
|
||||
o("contact removed", async function () {
|
||||
const { app, typeId } = ContactTypeRef
|
||||
const [instanceListId, instanceId] = existingContact._id
|
||||
await model.initWithTemplate({ to: recipients }, "subj", "", [], true, "a@b.c", false)
|
||||
await model.handleEntityEvent({
|
||||
application: app,
|
||||
typeId: typeId,
|
||||
typeRef: ContactTypeRef,
|
||||
operation: OperationType.DELETE,
|
||||
instanceListId,
|
||||
instanceId,
|
||||
type: "Contact",
|
||||
})
|
||||
o(model.allRecipients().length).equals(1)
|
||||
const updatedContact = model.allRecipients().find((r) => r.contact && isSameId(r.contact._id, existingContact._id))
|
||||
|
|
|
@ -347,12 +347,10 @@ o.spec("ConversationListModelTest", () => {
|
|||
o(model.getLabelsForMail(someMail.mail)[1]).notDeepEquals(labels[1])
|
||||
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailFolderTypeRef.app,
|
||||
typeId: MailFolderTypeRef.typeId,
|
||||
typeRef: MailFolderTypeRef,
|
||||
instanceListId: getListId(labels[1]),
|
||||
instanceId: getElementId(labels[1]),
|
||||
operation: OperationType.DELETE,
|
||||
type: "MailFolder",
|
||||
}
|
||||
|
||||
entityUpdateData.operation = OperationType.UPDATE
|
||||
|
@ -369,12 +367,10 @@ o.spec("ConversationListModelTest", () => {
|
|||
await model.loadInitial()
|
||||
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailFolderTypeRef.app,
|
||||
typeId: MailFolderTypeRef.typeId,
|
||||
typeRef: MailFolderTypeRef,
|
||||
instanceListId: getListId(labels[1]),
|
||||
instanceId: getElementId(labels[1]),
|
||||
operation: OperationType.DELETE,
|
||||
type: "MailFolder",
|
||||
}
|
||||
entityUpdateData.operation = OperationType.DELETE
|
||||
|
||||
|
@ -389,12 +385,10 @@ o.spec("ConversationListModelTest", () => {
|
|||
const someMail: LoadedMail = model._getMailMap().get(elementIdPart(makeMailId(someIndex)))!
|
||||
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailSetEntryTypeRef.app,
|
||||
typeId: MailSetEntryTypeRef.typeId,
|
||||
typeRef: MailSetEntryTypeRef,
|
||||
instanceListId: listIdPart(someMail.mailSetEntryId),
|
||||
instanceId: elementIdPart(someMail.mailSetEntryId),
|
||||
operation: OperationType.DELETE,
|
||||
type: "MailSetEntry",
|
||||
}
|
||||
|
||||
const oldItems = model.mails
|
||||
|
@ -428,12 +422,10 @@ o.spec("ConversationListModelTest", () => {
|
|||
})
|
||||
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailSetEntryTypeRef.app,
|
||||
typeId: MailSetEntryTypeRef.typeId,
|
||||
typeRef: MailSetEntryTypeRef,
|
||||
instanceListId: getListId(newEntry),
|
||||
instanceId: getElementId(newEntry),
|
||||
operation: OperationType.CREATE,
|
||||
type: "MailSetEntry",
|
||||
}
|
||||
|
||||
when(entityClient.load(MailSetEntryTypeRef, newEntry._id)).thenResolve(newEntry)
|
||||
|
@ -513,12 +505,10 @@ o.spec("ConversationListModelTest", () => {
|
|||
const newItems = [...oldItems]
|
||||
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailSetEntryTypeRef.app,
|
||||
typeId: MailSetEntryTypeRef.typeId,
|
||||
typeRef: MailSetEntryTypeRef,
|
||||
instanceListId: mailSetEntriesListId,
|
||||
instanceId: makeMailSetElementId(0),
|
||||
operation: OperationType.DELETE,
|
||||
type: "MailSetEntry",
|
||||
}
|
||||
|
||||
o(model.mails).deepEquals(oldMails)
|
||||
|
@ -540,12 +530,10 @@ o.spec("ConversationListModelTest", () => {
|
|||
const newItems = [oldMails[1]]
|
||||
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailSetEntryTypeRef.app,
|
||||
typeId: MailSetEntryTypeRef.typeId,
|
||||
typeRef: MailSetEntryTypeRef,
|
||||
instanceListId: mailSetEntriesListId,
|
||||
instanceId: makeMailSetElementId(2),
|
||||
operation: OperationType.DELETE,
|
||||
type: "MailSetEntry",
|
||||
}
|
||||
|
||||
o(model.mails).deepEquals(oldMails)
|
||||
|
@ -567,12 +555,10 @@ o.spec("ConversationListModelTest", () => {
|
|||
const newItems = [oldMails[1]]
|
||||
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailSetEntryTypeRef.app,
|
||||
typeId: MailSetEntryTypeRef.typeId,
|
||||
typeRef: MailSetEntryTypeRef,
|
||||
instanceListId: mailSetEntriesListId,
|
||||
instanceId: makeMailSetElementId(1),
|
||||
operation: OperationType.DELETE,
|
||||
type: "MailSetEntry",
|
||||
}
|
||||
|
||||
o(model.mails).deepEquals(oldMails)
|
||||
|
@ -605,12 +591,10 @@ o.spec("ConversationListModelTest", () => {
|
|||
mail.sets = [mailSet._id] // remove all labels
|
||||
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailTypeRef.app,
|
||||
typeId: MailTypeRef.typeId,
|
||||
typeRef: MailTypeRef,
|
||||
instanceListId: getListId(mail),
|
||||
instanceId: getElementId(mail),
|
||||
operation: OperationType.UPDATE,
|
||||
type: "Mail",
|
||||
}
|
||||
when(entityClient.load(MailTypeRef, mail._id)).thenResolve(mail)
|
||||
|
||||
|
@ -625,12 +609,10 @@ o.spec("ConversationListModelTest", () => {
|
|||
await model.loadInitial()
|
||||
const mail = { ...model.mails[2] }
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailTypeRef.app,
|
||||
typeId: MailTypeRef.typeId,
|
||||
typeRef: MailTypeRef,
|
||||
instanceListId: getListId(mail),
|
||||
instanceId: getElementId(mail),
|
||||
operation: OperationType.UPDATE,
|
||||
type: "Mail",
|
||||
}
|
||||
when(entityClient.load(MailTypeRef, mail._id)).thenResolve(mail)
|
||||
entityUpdateData.operation = OperationType.DELETE
|
||||
|
|
|
@ -307,12 +307,10 @@ o.spec("MailListModelTest", () => {
|
|||
o(model.getLabelsForMail(someMail.mail)[1]).notDeepEquals(labels[1])
|
||||
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailFolderTypeRef.app,
|
||||
typeId: MailFolderTypeRef.typeId,
|
||||
typeRef: MailFolderTypeRef,
|
||||
instanceListId: getListId(labels[1]),
|
||||
instanceId: getElementId(labels[1]),
|
||||
operation: OperationType.DELETE,
|
||||
type: "MailFolder",
|
||||
}
|
||||
|
||||
entityUpdateData.operation = OperationType.UPDATE
|
||||
|
@ -329,12 +327,10 @@ o.spec("MailListModelTest", () => {
|
|||
await model.loadInitial()
|
||||
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailFolderTypeRef.app,
|
||||
typeId: MailFolderTypeRef.typeId,
|
||||
typeRef: MailFolderTypeRef,
|
||||
instanceListId: getListId(labels[1]),
|
||||
instanceId: getElementId(labels[1]),
|
||||
operation: OperationType.DELETE,
|
||||
type: "MailFolder",
|
||||
}
|
||||
entityUpdateData.operation = OperationType.DELETE
|
||||
|
||||
|
@ -349,12 +345,10 @@ o.spec("MailListModelTest", () => {
|
|||
const someMail = model._loadedMails()[someIndex]
|
||||
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailSetEntryTypeRef.app,
|
||||
typeId: MailSetEntryTypeRef.typeId,
|
||||
typeRef: MailSetEntryTypeRef,
|
||||
instanceListId: listIdPart(someMail.mailSetEntryId),
|
||||
instanceId: elementIdPart(someMail.mailSetEntryId),
|
||||
operation: OperationType.DELETE,
|
||||
type: "MailSetEntry",
|
||||
}
|
||||
|
||||
const oldItems = model.items
|
||||
|
@ -384,12 +378,10 @@ o.spec("MailListModelTest", () => {
|
|||
})
|
||||
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailSetEntryTypeRef.app,
|
||||
typeId: MailSetEntryTypeRef.typeId,
|
||||
typeRef: MailSetEntryTypeRef,
|
||||
instanceListId: getListId(newEntry),
|
||||
instanceId: getElementId(newEntry),
|
||||
operation: OperationType.CREATE,
|
||||
type: "MailSetEntry",
|
||||
}
|
||||
|
||||
when(entityClient.load(MailSetEntryTypeRef, newEntry._id)).thenResolve(newEntry)
|
||||
|
@ -440,12 +432,10 @@ o.spec("MailListModelTest", () => {
|
|||
mail.sets = [mailSet._id] // remove all labels
|
||||
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailTypeRef.app,
|
||||
typeId: MailTypeRef.typeId,
|
||||
typeRef: MailTypeRef,
|
||||
instanceListId: getListId(mail),
|
||||
instanceId: getElementId(mail),
|
||||
operation: OperationType.UPDATE,
|
||||
type: "Mail",
|
||||
}
|
||||
when(entityClient.load(MailTypeRef, mail._id)).thenResolve(mail)
|
||||
|
||||
|
@ -460,12 +450,10 @@ o.spec("MailListModelTest", () => {
|
|||
await model.loadInitial()
|
||||
const mail = { ...model.items[2] }
|
||||
const entityUpdateData: EntityUpdateData = {
|
||||
application: MailTypeRef.app,
|
||||
typeId: MailTypeRef.typeId,
|
||||
typeRef: MailTypeRef,
|
||||
instanceListId: getListId(mail),
|
||||
instanceId: getElementId(mail),
|
||||
operation: OperationType.UPDATE,
|
||||
type: "Mail",
|
||||
}
|
||||
when(entityClient.load(MailTypeRef, mail._id)).thenResolve(mail)
|
||||
entityUpdateData.operation = OperationType.DELETE
|
||||
|
|
|
@ -21,6 +21,7 @@ import { isSameId } from "../../../../src/common/api/common/utils/EntityUtils.js
|
|||
import { createTestEntity } from "../../TestUtils.js"
|
||||
import { MailboxDetail, MailboxModel } from "../../../../src/common/mailFunctionality/MailboxModel.js"
|
||||
import { MailModel } from "../../../../src/mail-app/mail/model/MailModel.js"
|
||||
import { ClientModelInfo } from "../../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
o.spec("ConversationViewModel", function () {
|
||||
let conversation: ConversationEntry[]
|
||||
|
@ -55,7 +56,7 @@ o.spec("ConversationViewModel", function () {
|
|||
async function makeViewModel(pMail: Mail): Promise<void> {
|
||||
const factory = await viewModelFactory()
|
||||
const mailboxProperties = createTestEntity(MailboxPropertiesTypeRef)
|
||||
const entityClient = new EntityClient(entityRestClientMock)
|
||||
const entityClient = new EntityClient(entityRestClientMock, ClientModelInfo.getNewInstanceForTestsOnly())
|
||||
|
||||
const eventController: EventController = {
|
||||
addEntityListener: (listener) => {
|
||||
|
@ -248,12 +249,10 @@ o.spec("ConversationViewModel", function () {
|
|||
await eventCallback(
|
||||
[
|
||||
{
|
||||
application: "tutanota",
|
||||
typeId: ConversationEntryTypeRef.typeId,
|
||||
typeRef: ConversationEntryTypeRef,
|
||||
operation: OperationType.CREATE,
|
||||
instanceListId: listId,
|
||||
instanceId: yetAnotherMail.conversationEntry[1],
|
||||
type: "ConversationEntry",
|
||||
},
|
||||
],
|
||||
"mailGroupId",
|
||||
|
@ -287,12 +286,10 @@ o.spec("ConversationViewModel", function () {
|
|||
await eventCallback(
|
||||
[
|
||||
{
|
||||
application: "tutanota",
|
||||
typeId: ConversationEntryTypeRef.typeId,
|
||||
typeRef: ConversationEntryTypeRef,
|
||||
operation: OperationType.UPDATE,
|
||||
instanceListId: listId,
|
||||
instanceId: anotherMail.conversationEntry[1],
|
||||
type: "ConversationEntry",
|
||||
},
|
||||
],
|
||||
"mailGroupId",
|
||||
|
@ -315,12 +312,10 @@ o.spec("ConversationViewModel", function () {
|
|||
await eventCallback(
|
||||
[
|
||||
{
|
||||
application: "tutanota",
|
||||
typeId: ConversationEntryTypeRef.typeId,
|
||||
typeRef: ConversationEntryTypeRef,
|
||||
operation: OperationType.CREATE,
|
||||
instanceListId: listId,
|
||||
instanceId: yetAnotherMail.conversationEntry[1],
|
||||
type: "ConversationEntry",
|
||||
},
|
||||
],
|
||||
"mailGroupId",
|
||||
|
@ -341,12 +336,10 @@ o.spec("ConversationViewModel", function () {
|
|||
await eventCallback(
|
||||
[
|
||||
{
|
||||
application: "tutanota",
|
||||
typeId: ConversationEntryTypeRef.typeId,
|
||||
typeRef: ConversationEntryTypeRef,
|
||||
operation: OperationType.CREATE,
|
||||
instanceListId: listId,
|
||||
instanceId: yetAnotherMail.conversationEntry[1],
|
||||
type: "ConversationEntry",
|
||||
},
|
||||
],
|
||||
"mailGroupId",
|
||||
|
@ -382,12 +375,10 @@ o.spec("ConversationViewModel", function () {
|
|||
await eventCallback(
|
||||
[
|
||||
{
|
||||
application: "tutanota",
|
||||
typeId: ConversationEntryTypeRef.typeId,
|
||||
typeRef: ConversationEntryTypeRef,
|
||||
operation: OperationType.UPDATE,
|
||||
instanceListId: listId,
|
||||
instanceId: trashDraftMail.conversationEntry[1],
|
||||
type: "ConversationEntry",
|
||||
},
|
||||
],
|
||||
"mailGroupId",
|
||||
|
|
|
@ -2,6 +2,7 @@ import o from "@tutao/otest"
|
|||
import { client } from "../../../src/common/misc/ClientDetector.js"
|
||||
import { Mode } from "../../../src/common/api/common/Env.js"
|
||||
import { AppType, BrowserType, DeviceType } from "../../../src/common/misc/ClientConstants.js"
|
||||
import { withOverriddenEnv } from "../TestUtils"
|
||||
|
||||
o.spec("ClientDetector test", function () {
|
||||
o("ClientDetector detect chrome windows", () => {
|
||||
|
@ -196,14 +197,6 @@ o.spec("ClientDetector test", function () {
|
|||
o(client.isMobileDevice()).equals(true)
|
||||
})
|
||||
o.spec("app", function () {
|
||||
let prevMode
|
||||
o.before(function () {
|
||||
prevMode = env.mode
|
||||
env.mode = Mode.App
|
||||
})
|
||||
o.after(function () {
|
||||
env.mode = prevMode
|
||||
})
|
||||
o("ClientDetector the android 4 in app mode supported", () => {
|
||||
client.init(
|
||||
"Mozilla/5.0 (Linux U Android 4.0, de-de HTC_Desire_X Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
|
||||
|
@ -238,23 +231,19 @@ o.spec("ClientDetector test", function () {
|
|||
o(client.device).equals(DeviceType.DESKTOP)
|
||||
o(client.isMobileDevice()).equals(false)
|
||||
})
|
||||
o("ClientDetector firefox os is supported", () => {
|
||||
env.mode = Mode.App
|
||||
client.init("Mozilla/5.0 (Mobile rv:26.0) Gecko/26.0 Firefox/26.0", "Linux")
|
||||
o("ClientDetector firefox os is supported", async () => {
|
||||
await withOverriddenEnv({ mode: Mode.App }, () => client.init("Mozilla/5.0 (Mobile rv:26.0) Gecko/26.0 Firefox/26.0", "Linux"))
|
||||
o(client.browser).equals(BrowserType.FIREFOX)
|
||||
o(client.browserVersion).equals(26)
|
||||
o(client.device).equals(DeviceType.OTHER_MOBILE)
|
||||
o(client.isMobileDevice()).equals(true)
|
||||
env.mode = Mode.Browser
|
||||
})
|
||||
o("ClientDetector firefox os tablet is supported", () => {
|
||||
env.mode = Mode.App
|
||||
client.init("Mozilla/5.0 (Tablet rv:26.0) Gecko/26.0 Firefox/26.0", "Linux")
|
||||
o("ClientDetector firefox os tablet is supported", async () => {
|
||||
await withOverriddenEnv({ mode: Mode.App }, () => client.init("Mozilla/5.0 (Tablet rv:26.0) Gecko/26.0 Firefox/26.0", "Linux"))
|
||||
o(client.browser).equals(BrowserType.FIREFOX)
|
||||
o(client.browserVersion).equals(26)
|
||||
o(client.device).equals(DeviceType.OTHER_MOBILE)
|
||||
o(client.isMobileDevice()).equals(true)
|
||||
env.mode = Mode.Browser
|
||||
})
|
||||
})
|
||||
o("old Chrome is not supported", function () {
|
||||
|
@ -282,61 +271,68 @@ o.spec("ClientDetector test", function () {
|
|||
})
|
||||
|
||||
o.spec("ClientDetector AppType test", function () {
|
||||
o.beforeEach(function () {
|
||||
env.mode = Mode.App
|
||||
})
|
||||
o("ClientDetector detect calendar app on Android", () => {
|
||||
client.init(
|
||||
"Mozilla/5.0 (Linux Android 4.1.1 HTC Desire X Build/JRO03C) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.99 Mobile Safari/537.36",
|
||||
"Linux",
|
||||
AppType.Calendar,
|
||||
)
|
||||
o(client.device).equals(DeviceType.ANDROID)
|
||||
o(client.isMobileDevice()).equals(true)
|
||||
o(client.getIdentifier()).equals("Android Calendar App")
|
||||
o("ClientDetector detect calendar app on Android", async () => {
|
||||
await withOverriddenEnv({ mode: Mode.App }, () => {
|
||||
client.init(
|
||||
"Mozilla/5.0 (Linux Android 4.1.1 HTC Desire X Build/JRO03C) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.99 Mobile Safari/537.36",
|
||||
"Linux",
|
||||
AppType.Calendar,
|
||||
)
|
||||
o(client.device).equals(DeviceType.ANDROID)
|
||||
o(client.isMobileDevice()).equals(true)
|
||||
o(client.getIdentifier()).equals("Android Calendar App")
|
||||
})
|
||||
})
|
||||
|
||||
o("ClientDetector detect calendar app on iPhone", () => {
|
||||
client.init(
|
||||
"Mozilla/5.0 (iPhone CPU iPhone OS 7_0_2 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A501 Safari/9537.53",
|
||||
"Linux",
|
||||
AppType.Calendar,
|
||||
)
|
||||
o(client.device).equals(DeviceType.IPHONE)
|
||||
o(client.isMobileDevice()).equals(true)
|
||||
o(client.getIdentifier()).equals("iPhone Calendar App")
|
||||
o("ClientDetector detect calendar app on iPhone", async () => {
|
||||
await withOverriddenEnv({ mode: Mode.App }, () => {
|
||||
client.init(
|
||||
"Mozilla/5.0 (iPhone CPU iPhone OS 7_0_2 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A501 Safari/9537.53",
|
||||
"Linux",
|
||||
AppType.Calendar,
|
||||
)
|
||||
o(client.device).equals(DeviceType.IPHONE)
|
||||
o(client.isMobileDevice()).equals(true)
|
||||
o(client.getIdentifier()).equals("iPhone Calendar App")
|
||||
})
|
||||
})
|
||||
|
||||
o("ClientDetector detect mail app on Android", () => {
|
||||
client.init(
|
||||
"Mozilla/5.0 (Linux Android 4.1.1 HTC Desire X Build/JRO03C) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.99 Mobile Safari/537.36",
|
||||
"Linux",
|
||||
AppType.Mail,
|
||||
)
|
||||
o(client.device).equals(DeviceType.ANDROID)
|
||||
o(client.isMobileDevice()).equals(true)
|
||||
o(client.getIdentifier()).equals("Android Mail App")
|
||||
o("ClientDetector detect mail app on Android", async () => {
|
||||
await withOverriddenEnv({ mode: Mode.App }, () => {
|
||||
client.init(
|
||||
"Mozilla/5.0 (Linux Android 4.1.1 HTC Desire X Build/JRO03C) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.99 Mobile Safari/537.36",
|
||||
"Linux",
|
||||
AppType.Mail,
|
||||
)
|
||||
o(client.device).equals(DeviceType.ANDROID)
|
||||
o(client.isMobileDevice()).equals(true)
|
||||
o(client.getIdentifier()).equals("Android Mail App")
|
||||
})
|
||||
})
|
||||
|
||||
o("ClientDetector detect mail app on iPhone", () => {
|
||||
client.init(
|
||||
"Mozilla/5.0 (iPhone CPU iPhone OS 7_0_2 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A501 Safari/9537.53",
|
||||
"Linux",
|
||||
AppType.Mail,
|
||||
)
|
||||
o(client.device).equals(DeviceType.IPHONE)
|
||||
o(client.isMobileDevice()).equals(true)
|
||||
o(client.getIdentifier()).equals("iPhone Mail App")
|
||||
o("ClientDetector detect mail app on iPhone", async () => {
|
||||
await withOverriddenEnv({ mode: Mode.App }, () => {
|
||||
client.init(
|
||||
"Mozilla/5.0 (iPhone CPU iPhone OS 7_0_2 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A501 Safari/9537.53",
|
||||
"Linux",
|
||||
AppType.Mail,
|
||||
)
|
||||
o(client.device).equals(DeviceType.IPHONE)
|
||||
o(client.isMobileDevice()).equals(true)
|
||||
o(client.getIdentifier()).equals("iPhone Mail App")
|
||||
})
|
||||
})
|
||||
|
||||
o("ClientDetector throws on wrong configuration", () => {
|
||||
client.init(
|
||||
"Mozilla/5.0 (iPhone CPU iPhone OS 7_0_2 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A501 Safari/9537.53",
|
||||
"Linux",
|
||||
AppType.Integrated,
|
||||
)
|
||||
o(client.device).equals(DeviceType.IPHONE)
|
||||
o(client.isMobileDevice()).equals(true)
|
||||
o(() => client.getIdentifier()).throws("AppType.Integrated is not allowed for mobile apps")
|
||||
o("ClientDetector throws on wrong configuration", async () => {
|
||||
await withOverriddenEnv({ mode: Mode.App }, () => {
|
||||
client.init(
|
||||
"Mozilla/5.0 (iPhone CPU iPhone OS 7_0_2 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A501 Safari/9537.53",
|
||||
"Linux",
|
||||
AppType.Integrated,
|
||||
)
|
||||
o(client.device).equals(DeviceType.IPHONE)
|
||||
o(client.isMobileDevice()).equals(true)
|
||||
o(() => client.getIdentifier()).throws("AppType.Integrated is not allowed for mobile apps")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -32,6 +32,7 @@ import { UserController } from "../../../src/common/api/main/UserController.js"
|
|||
import { createUserSettingsGroupRoot, UserSettingsGroupRootTypeRef } from "../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { EventController } from "../../../src/common/api/main/EventController.js"
|
||||
import { createTestEntity } from "../TestUtils.js"
|
||||
import { ClientModelInfo } from "../../../src/common/api/common/EntityFunctions"
|
||||
|
||||
const { anything } = matchers
|
||||
|
||||
|
@ -93,6 +94,7 @@ o.spec("UsageTestModel", function () {
|
|||
|
||||
ephemeralStorage = new EphemeralUsageTestStorage()
|
||||
persistentStorage = new EphemeralUsageTestStorage()
|
||||
let clientModelResolver = ClientModelInfo.getNewInstanceForTestsOnly()
|
||||
usageTestModel = new UsageTestModel(
|
||||
{
|
||||
[StorageBehavior.Persist]: persistentStorage,
|
||||
|
@ -104,6 +106,7 @@ o.spec("UsageTestModel", function () {
|
|||
loginControllerMock,
|
||||
eventControllerMock,
|
||||
() => usageTestController,
|
||||
clientModelResolver,
|
||||
)
|
||||
|
||||
replace(usageTestModel, "customerProperties", createTestEntity(CustomerPropertiesTypeRef, { usageDataOptedOut: false }))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue