2024-07-01 17:56:41 +02:00
|
|
|
import type { BrowserData } from "../../src/common/misc/ClientConstants.js"
|
2025-03-13 16:37:55 +01:00
|
|
|
import { DbEncryptionData } from "../../src/common/api/worker/search/SearchTypes.js"
|
2024-08-20 18:03:03 +02:00
|
|
|
import { IndexerCore } from "../../src/mail-app/workerUtils/index/IndexerCore.js"
|
2024-07-01 17:56:41 +02:00
|
|
|
import { DbFacade, DbTransaction } from "../../src/common/api/worker/search/DbFacade.js"
|
2025-03-13 16:37:55 +01:00
|
|
|
import { assertNotNull, clone, deepEqual, defer, Thunk, typedEntries, TypeRef } from "@tutao/tutanota-utils"
|
2024-07-01 17:56:41 +02:00
|
|
|
import type { DesktopKeyStoreFacade } from "../../src/common/desktop/DesktopKeyStoreFacade.js"
|
2022-12-27 15:37:40 +01:00
|
|
|
import { mock } from "@tutao/tutanota-test-utils"
|
|
|
|
import { aes256RandomKey, fixedIv, uint8ArrayToKey } from "@tutao/tutanota-crypto"
|
2024-07-01 17:56:41 +02:00
|
|
|
import { ScheduledPeriodicId, ScheduledTimeoutId, Scheduler } from "../../src/common/api/common/utils/Scheduler.js"
|
2024-03-11 16:25:43 +01:00
|
|
|
import { matchers, object, when } from "testdouble"
|
2024-12-09 15:31:25 +01:00
|
|
|
import { Entity, ModelValue, TypeModel } from "../../src/common/api/common/EntityTypes.js"
|
2024-07-01 17:56:41 +02:00
|
|
|
import { create } from "../../src/common/api/common/utils/EntityUtils.js"
|
2025-05-19 16:11:26 +02:00
|
|
|
import { ClientModelInfo, ServerModelInfo, ServerModels, TypeModelResolver } from "../../src/common/api/common/EntityFunctions.js"
|
2024-03-11 16:25:43 +01:00
|
|
|
import { type fetch as undiciFetch, type Response } from "undici"
|
2024-12-09 15:31:25 +01:00
|
|
|
import { Cardinality, ValueType } from "../../src/common/api/common/EntityConstants.js"
|
2025-05-16 16:03:24 +02:00
|
|
|
import { InstancePipeline } from "../../src/common/api/worker/crypto/InstancePipeline"
|
|
|
|
import { ModelMapper } from "../../src/common/api/worker/crypto/ModelMapper"
|
2025-06-03 12:08:18 +02:00
|
|
|
import { dummyResolver } from "./api/worker/crypto/InstancePipelineTestUtils"
|
2025-03-13 16:37:55 +01:00
|
|
|
import { EncryptedDbWrapper } from "../../src/common/api/worker/search/EncryptedDbWrapper"
|
2018-10-12 10:50:17 +02:00
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
export const browserDataStub: BrowserData = {
|
|
|
|
needsMicrotaskHack: false,
|
|
|
|
needsExplicitIDBIds: false,
|
2022-12-27 15:37:40 +01:00
|
|
|
indexedDbSupported: true,
|
2022-01-07 15:58:30 +01:00
|
|
|
}
|
2018-10-12 10:50:17 +02:00
|
|
|
|
2022-12-27 15:37:40 +01:00
|
|
|
export function makeCore(
|
|
|
|
args?: {
|
2025-03-13 16:37:55 +01:00
|
|
|
encryptionData?: DbEncryptionData
|
2022-12-27 15:37:40 +01:00
|
|
|
browserData?: BrowserData
|
|
|
|
transaction?: DbTransaction
|
|
|
|
},
|
|
|
|
mocker?: (_: any) => void,
|
|
|
|
): IndexerCore {
|
2022-01-13 13:24:37 +01:00
|
|
|
const safeArgs = args ?? {}
|
2022-12-27 15:37:40 +01:00
|
|
|
const { transaction } = safeArgs
|
2025-03-13 16:37:55 +01:00
|
|
|
const dbFacade = { createTransaction: () => Promise.resolve(transaction) } as Partial<DbFacade>
|
|
|
|
const defaultDb = new EncryptedDbWrapper(dbFacade as DbFacade)
|
|
|
|
defaultDb.init(safeArgs.encryptionData ?? { key: aes256RandomKey(), iv: fixedIv })
|
|
|
|
const { db, browserData } = {
|
|
|
|
...{ db: defaultDb, browserData: browserDataStub },
|
2022-01-13 13:24:37 +01:00
|
|
|
...safeArgs,
|
2018-10-12 10:50:17 +02:00
|
|
|
}
|
2025-03-13 16:37:55 +01:00
|
|
|
const core = new IndexerCore(db, browserData)
|
2025-01-03 10:16:07 +01:00
|
|
|
if (mocker) mock(core, mocker)
|
2018-10-12 10:50:17 +02:00
|
|
|
return core
|
2019-03-12 11:58:31 +01:00
|
|
|
}
|
2020-01-30 18:58:00 +01:00
|
|
|
|
2023-11-10 16:26:04 +01:00
|
|
|
export function makeKeyStoreFacade(uint8ArrayKey: Uint8Array): DesktopKeyStoreFacade {
|
|
|
|
const o: DesktopKeyStoreFacade = object()
|
|
|
|
when(o.getDeviceKey()).thenResolve(uint8ArrayToKey(uint8ArrayKey))
|
2024-01-22 17:24:34 +01:00
|
|
|
when(o.getKeyChainKey()).thenResolve(uint8ArrayToKey(uint8ArrayKey))
|
2023-11-10 16:26:04 +01:00
|
|
|
return o
|
2022-03-01 17:30:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type IdThunk = {
|
|
|
|
id: ScheduledTimeoutId
|
|
|
|
thunk: Thunk
|
|
|
|
}
|
|
|
|
|
|
|
|
export class SchedulerMock implements Scheduler {
|
|
|
|
alarmId: number = 0
|
|
|
|
|
|
|
|
/** key is the time */
|
|
|
|
scheduledAt: Map<number, IdThunk> = new Map()
|
2024-03-11 16:25:43 +01:00
|
|
|
scheduledAfter: Map<number, IdThunk> = new Map()
|
2022-03-01 17:30:19 +01:00
|
|
|
cancelledAt: Set<ScheduledTimeoutId> = new Set()
|
|
|
|
scheduledPeriodic: Map<number, IdThunk> = new Map()
|
|
|
|
cancelledPeriodic: Set<ScheduledTimeoutId> = new Set()
|
|
|
|
|
|
|
|
scheduleAt(callback, date): ScheduledTimeoutId {
|
|
|
|
const id = this._incAlarmId()
|
|
|
|
|
|
|
|
this.scheduledAt.set(date.getTime(), {
|
|
|
|
id,
|
|
|
|
thunk: callback,
|
|
|
|
})
|
|
|
|
return id
|
|
|
|
}
|
|
|
|
|
2024-03-11 16:25:43 +01:00
|
|
|
getThunkAt(time: number): Thunk {
|
|
|
|
return assertNotNull(this.scheduledAt.get(time), "No thunk scheduled at " + time).thunk
|
|
|
|
}
|
|
|
|
|
|
|
|
getThunkAfter(time: number): Thunk {
|
|
|
|
return assertNotNull(this.scheduledAfter.get(time), "No thunk scheduled after " + time).thunk
|
|
|
|
}
|
|
|
|
|
|
|
|
scheduleAfter(thunk: Thunk, after: number): ScheduledTimeoutId {
|
|
|
|
const id = this._incAlarmId()
|
|
|
|
|
|
|
|
this.scheduledAfter.set(after, {
|
|
|
|
id,
|
|
|
|
thunk: thunk,
|
|
|
|
})
|
|
|
|
return id
|
|
|
|
}
|
|
|
|
|
2022-03-01 17:30:19 +01:00
|
|
|
unscheduleTimeout(id) {
|
|
|
|
this.cancelledAt.add(id)
|
|
|
|
}
|
|
|
|
|
|
|
|
schedulePeriodic(thunk, period: number): ScheduledPeriodicId {
|
|
|
|
const id = this._incAlarmId()
|
2022-12-27 15:37:40 +01:00
|
|
|
this.scheduledPeriodic.set(period, { id, thunk })
|
2022-03-01 17:30:19 +01:00
|
|
|
return id
|
|
|
|
}
|
|
|
|
|
2024-03-11 16:25:43 +01:00
|
|
|
getThunkPeriodic(period: number): Thunk {
|
|
|
|
return assertNotNull(this.scheduledPeriodic.get(period), "No thunk scheduled each " + period).thunk
|
|
|
|
}
|
|
|
|
|
|
|
|
getAllPeriodThunks(): Array<Thunk> {
|
|
|
|
return Array.from(this.scheduledPeriodic.values()).map((idThunk) => idThunk.thunk)
|
|
|
|
}
|
|
|
|
|
2022-03-01 17:30:19 +01:00
|
|
|
unschedulePeriodic(id: ScheduledPeriodicId) {
|
|
|
|
this.cancelledPeriodic.add(id)
|
|
|
|
}
|
|
|
|
|
|
|
|
_incAlarmId(): ScheduledTimeoutId {
|
|
|
|
return this.alarmId++
|
|
|
|
}
|
2022-12-27 15:37:40 +01:00
|
|
|
}
|
2023-09-26 18:03:30 +02:00
|
|
|
|
|
|
|
export const domainConfigStub: DomainConfig = {
|
|
|
|
firstPartyDomain: true,
|
2023-10-18 12:05:21 +02:00
|
|
|
partneredDomainTransitionUrl: "",
|
2023-09-26 18:03:30 +02:00
|
|
|
apiUrl: "",
|
|
|
|
u2fAppId: "",
|
|
|
|
webauthnRpId: "",
|
|
|
|
referralBaseUrl: "",
|
|
|
|
giftCardBaseUrl: "",
|
|
|
|
paymentUrl: "",
|
|
|
|
webauthnUrl: "",
|
|
|
|
legacyWebauthnUrl: "",
|
2023-10-09 17:05:07 +02:00
|
|
|
webauthnMobileUrl: "",
|
|
|
|
legacyWebauthnMobileUrl: "",
|
2023-10-19 16:17:24 +02:00
|
|
|
websiteBaseUrl: "",
|
2023-09-26 18:03:30 +02:00
|
|
|
}
|
2023-11-09 17:04:42 +01:00
|
|
|
|
2023-11-20 17:08:24 +01:00
|
|
|
// non-async copy of the function
|
2023-11-09 17:04:42 +01:00
|
|
|
function resolveTypeReference(typeRef: TypeRef<any>): TypeModel {
|
2025-05-16 16:03:24 +02:00
|
|
|
const modelMap = ClientModelInfo.getNewInstanceForTestsOnly().typeModels[typeRef.app]
|
2025-03-10 16:19:11 +01:00
|
|
|
const typeModel = modelMap[typeRef.typeId]
|
2023-11-09 17:04:42 +01:00
|
|
|
|
|
|
|
if (typeModel == null) {
|
|
|
|
throw new Error("Cannot find TypeRef: " + JSON.stringify(typeRef))
|
|
|
|
} else {
|
|
|
|
return typeModel
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-09 15:31:25 +01:00
|
|
|
// copy of the _getDefaultValue but with Date(0) being default date so that the tests are deterministic
|
|
|
|
function getDefaultTestValue(valueName: string, value: ModelValue): any {
|
|
|
|
if (valueName === "_format") {
|
|
|
|
return "0"
|
|
|
|
} else if (valueName === "_id") {
|
2025-03-13 16:37:55 +01:00
|
|
|
return `${value.id}_id`
|
2024-12-09 15:31:25 +01:00
|
|
|
} else if (valueName === "_permissions") {
|
2025-03-13 16:37:55 +01:00
|
|
|
return `${value.id}_permissions`
|
2024-12-09 15:31:25 +01:00
|
|
|
} else if (value.cardinality === Cardinality.ZeroOrOne) {
|
|
|
|
return null
|
|
|
|
} else {
|
|
|
|
switch (value.type) {
|
|
|
|
case ValueType.Bytes:
|
|
|
|
return new Uint8Array(0)
|
|
|
|
|
|
|
|
case ValueType.Date:
|
|
|
|
return new Date(0)
|
|
|
|
|
|
|
|
case ValueType.Number:
|
|
|
|
return "0"
|
|
|
|
|
|
|
|
case ValueType.String:
|
|
|
|
return ""
|
|
|
|
|
|
|
|
case ValueType.Boolean:
|
|
|
|
return false
|
|
|
|
|
|
|
|
case ValueType.CustomId:
|
|
|
|
case ValueType.GeneratedId:
|
2025-03-13 16:37:55 +01:00
|
|
|
return `${value.id}_${valueName}`
|
2024-12-09 15:31:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-13 16:37:55 +01:00
|
|
|
export function createTestEntity<T extends Entity>(
|
|
|
|
typeRef: TypeRef<T>,
|
|
|
|
values?: Partial<T>,
|
|
|
|
opts?: {
|
|
|
|
populateAggregates: boolean
|
|
|
|
},
|
|
|
|
): T {
|
2023-11-09 17:04:42 +01:00
|
|
|
const typeModel = resolveTypeReference(typeRef as TypeRef<any>)
|
2024-12-09 15:31:25 +01:00
|
|
|
const entity = create(typeModel, typeRef, getDefaultTestValue)
|
2025-03-13 16:37:55 +01:00
|
|
|
if (opts?.populateAggregates) {
|
|
|
|
for (const [_, assocDef] of typedEntries(typeModel.associations)) {
|
|
|
|
if (assocDef.cardinality === Cardinality.One) {
|
|
|
|
const assocName = assocDef.name
|
|
|
|
switch (assocDef.type) {
|
|
|
|
case "AGGREGATION": {
|
|
|
|
const assocTypeRef = new TypeRef<Entity>(assocDef.dependency ?? typeRef.app, assocDef.refTypeId)
|
|
|
|
entity[assocName] = createTestEntity(assocTypeRef, undefined, opts)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case "ELEMENT_ASSOCIATION":
|
|
|
|
entity[assocName] = `elementAssoc_${assocName}`
|
|
|
|
break
|
|
|
|
case "LIST_ASSOCIATION":
|
|
|
|
entity[assocName] = `listAssoc_${assocName}`
|
|
|
|
break
|
|
|
|
case "LIST_ELEMENT_ASSOCIATION_GENERATED":
|
|
|
|
case "LIST_ELEMENT_ASSOCIATION_CUSTOM":
|
|
|
|
entity[assocName] = [`listElemAssocList_${assocName}`, `listElemAssocElem_${assocName}`]
|
|
|
|
break
|
|
|
|
case "BLOB_ELEMENT_ASSOCIATION":
|
|
|
|
entity[assocName] = [`blobElemAssocList_${assocName}`, `blobElemAssocElem_${assocName}`]
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-11-09 17:04:42 +01:00
|
|
|
if (values) {
|
2023-11-10 16:59:39 +01:00
|
|
|
return Object.assign(entity, values)
|
2023-11-09 17:04:42 +01:00
|
|
|
} else {
|
|
|
|
return entity
|
|
|
|
}
|
|
|
|
}
|
2024-03-11 16:25:43 +01:00
|
|
|
|
2025-06-03 12:08:18 +02:00
|
|
|
export async function createTestEntityWithDummyResolver<T extends Entity>(typeRef: TypeRef<T>, values?: Partial<T>): Promise<T> {
|
|
|
|
const typeModel = await dummyResolver(typeRef)
|
|
|
|
const entity = create(typeModel, typeRef, getDefaultTestValue)
|
|
|
|
if (values) {
|
|
|
|
return Object.assign(entity, values)
|
|
|
|
} else {
|
|
|
|
return entity
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-11 16:25:43 +01:00
|
|
|
export function mockFetchRequest(mock: typeof undiciFetch, url: string, headers: Record<string, string>, status: number, jsonObject: unknown): Promise<void> {
|
|
|
|
const response = object<Writeable<Response>>()
|
|
|
|
response.ok = status >= 200 && status < 300
|
|
|
|
response.status = status
|
|
|
|
const jsonDefer = defer<void>()
|
|
|
|
when(response.json()).thenDo(() => {
|
|
|
|
jsonDefer.resolve()
|
|
|
|
return Promise.resolve(jsonObject)
|
|
|
|
})
|
|
|
|
when(
|
|
|
|
mock(
|
|
|
|
matchers.argThat((urlArg) => urlArg.toString() === url),
|
|
|
|
matchers.argThat((options) => {
|
|
|
|
return deepEqual(options.headers, headers)
|
|
|
|
}),
|
|
|
|
),
|
|
|
|
).thenResolve(response)
|
|
|
|
return jsonDefer.promise
|
|
|
|
}
|
|
|
|
|
|
|
|
export function textIncludes(match: string): (text: string) => { pass: true } | { pass: false; message: string } {
|
|
|
|
return (text) => (text.includes(match) ? { pass: true } : { pass: false, message: `should include: "${match}"` })
|
|
|
|
}
|
2025-02-18 19:09:24 +01:00
|
|
|
|
|
|
|
export function equalToArray<A extends any[]>(
|
|
|
|
expectedArray: A,
|
|
|
|
): (value: A) =>
|
|
|
|
| { pass: false; message: string }
|
|
|
|
| {
|
|
|
|
pass: true
|
|
|
|
} {
|
|
|
|
return (value) =>
|
|
|
|
deepEqual(value, expectedArray)
|
|
|
|
? { pass: true }
|
|
|
|
: {
|
|
|
|
pass: false,
|
|
|
|
message: `Arrays are different: Expected ${expectedArray.length} items but got ${value.length}.
|
|
|
|
The first expected item is ${JSON.stringify(expectedArray[0])} but got ${JSON.stringify(value[0])}.
|
|
|
|
The last expected item is ${JSON.stringify(expectedArray.at(-1))} but got ${JSON.stringify(value.at(-1))}`,
|
|
|
|
}
|
|
|
|
}
|
2025-03-10 16:19:11 +01:00
|
|
|
|
|
|
|
export function removeFinalIvs(instance: Entity): Entity {
|
|
|
|
delete instance["_finalIvs"]
|
|
|
|
const keys = Object.keys(instance)
|
|
|
|
for (const key of keys) {
|
|
|
|
const maybeAggregate = instance[key]
|
|
|
|
if (maybeAggregate instanceof Object) {
|
|
|
|
removeFinalIvs(maybeAggregate)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return instance
|
|
|
|
}
|
|
|
|
|
|
|
|
export function removeAggregateIds(instance: Entity, aggregate: boolean = false): Entity {
|
|
|
|
if (aggregate && instance["_id"]) {
|
|
|
|
instance["_id"] = null
|
|
|
|
}
|
|
|
|
const keys = Object.keys(instance)
|
|
|
|
for (const key of keys) {
|
|
|
|
const maybeAggregate = instance[key]
|
|
|
|
if (maybeAggregate instanceof Object) {
|
|
|
|
removeAggregateIds(maybeAggregate, true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return instance
|
|
|
|
}
|
2025-06-03 12:08:18 +02:00
|
|
|
|
2025-05-19 16:11:26 +02:00
|
|
|
export function clientModelAsServerModel(clientModel: ClientModelInfo): ServerModelInfo {
|
2025-03-10 16:19:11 +01:00
|
|
|
let models = Object.keys(clientModel.typeModels).reduce((obj, app) => {
|
|
|
|
Object.assign(obj, {
|
|
|
|
[app]: {
|
|
|
|
name: app,
|
|
|
|
version: clientModel.modelInfos[app].version,
|
2025-05-16 16:03:24 +02:00
|
|
|
types: clone(clientModel.typeModels[app]),
|
2025-03-10 16:19:11 +01:00
|
|
|
},
|
|
|
|
})
|
|
|
|
return obj
|
2025-05-19 16:11:26 +02:00
|
|
|
}, {}) as ServerModels
|
|
|
|
const modelInfo = ServerModelInfo.getUninitializedInstanceForTestsOnly(clientModel, () => {
|
|
|
|
throw new Error("should not fetch in test")
|
|
|
|
})
|
|
|
|
modelInfo.typeModels = models
|
|
|
|
return modelInfo
|
2025-03-10 16:19:11 +01:00
|
|
|
}
|
2025-05-16 16:03:24 +02:00
|
|
|
|
|
|
|
export function clientInitializedTypeModelResolver(): TypeModelResolver {
|
|
|
|
const clientModelInfo = ClientModelInfo.getNewInstanceForTestsOnly()
|
2025-05-19 16:11:26 +02:00
|
|
|
const serverModelInfo = clientModelAsServerModel(clientModelInfo)
|
|
|
|
return new TypeModelResolver(clientModelInfo, serverModelInfo)
|
2025-05-16 16:03:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|