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:
ivk 2025-05-16 16:03:24 +02:00 committed by map
parent b2e5f83f89
commit 9e31ee0409
87 changed files with 1778 additions and 1452 deletions

View file

@ -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;

View file

@ -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

View file

@ -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)

View file

@ -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))
})
}

View file

@ -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)

View file

@ -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()
}
}

View file

@ -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))
)
}

View file

@ -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)

View file

@ -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) {

View file

@ -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()

View file

@ -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) {

View file

@ -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)

View file

@ -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()

View file

@ -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,
{

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 &&

View file

@ -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)

View file

@ -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> {

View file

@ -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,

View file

@ -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> = []

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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"))

View file

@ -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)

View file

@ -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,

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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)

View file

@ -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)

View file

@ -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,
})
})
}

View file

@ -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

View file

@ -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

View file

@ -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) => {

View file

@ -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

View file

@ -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])

View file

@ -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)
}

View file

@ -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)

View file

@ -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()

View file

@ -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")

View file

@ -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]
}
}
}

View file

@ -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)
})
})

View file

@ -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 () {

View file

@ -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 })

View file

@ -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,

View file

@ -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",

View file

@ -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)

View file

@ -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 })
})
}

View file

@ -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 () {

View file

@ -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(

View file

@ -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),
)
})

View file

@ -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)
})

View file

@ -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)

View file

@ -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)

View file

@ -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))

View file

@ -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(

View file

@ -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",
}
}
}

View file

@ -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(

View file

@ -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"

View file

@ -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 () {

View file

@ -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 () {

View file

@ -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
}

View file

@ -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)
})

View file

@ -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)

View file

@ -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

View file

@ -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))

View file

@ -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(),
)
}

View file

@ -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("")

View file

@ -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(

View file

@ -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)

View file

@ -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)

View file

@ -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,
)
})

View file

@ -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()))

View file

@ -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)

View 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"])]

View file

@ -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,
}
}
})

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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")
})
})
})

View file

@ -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 }))