tutanota/test/tests/TestUtils.ts

360 lines
11 KiB
TypeScript
Raw Normal View History

import type { BrowserData } from "../../src/common/misc/ClientConstants.js"
Add SQLite search on clients where offline storage is available - Introduce a separate Indexer for SQLite using FTS5 - Split search backends and use the right one based on client (IndexedDB for Browser, and OfflineStorage everywhere else) - Split SearchFacade into two implementations - Adds a table for storing unindexed metadata for mails - Escape special character for SQLite search To escape special characters from fts5 syntax. However, simply surrounding each token in quotes is sufficient to do this. See section 3.1 "FTS5 Strings" here: https://www.sqlite.org/fts5.html which states that a string may be specified by surrounding it in quotes, and that special string requirements only exist for strings that are not in quotes. - Add EncryptedDbWrapper - Simplify out of sync logic in IndexedDbIndexer - Fix deadlock when initializing IndexedDbIndexer - Cleanup indexedDb index when migrating to offline storage index - Pass contactSuggestionFacade to IndexedDbSearchFacade The only suggestion facade used by IndexedDbSearchFacade was the contact suggestion facade. So we made it clearer. - Remove IndexerCore stats - Split custom cache handlers into separate files We were already doing this with user, so we should do this with the other entity types. - Rewrite IndexedDb tests - Add OfflineStorage indexer tests - Add custom cache handlers tests to OfflineStorageTest - Add tests for custom cache handlers with ephemeral storage - Use dbStub instead of dbMock in IndexedDbIndexerTest - Replace spy with testdouble in IndexedDbIndexerTest Close #8550 Co-authored-by: ivk <ivk@tutao.de> Co-authored-by: paw <paw-hub@users.noreply.github.com> Co-authored-by: wrd <wrd@tutao.de> Co-authored-by: bir <bir@tutao.de> Co-authored-by: hrb-hub <hrb-hub@users.noreply.github.com>
2025-03-13 16:37:55 +01:00
import { DbEncryptionData } from "../../src/common/api/worker/search/SearchTypes.js"
import { IndexerCore } from "../../src/mail-app/workerUtils/index/IndexerCore.js"
import { DbFacade, DbTransaction } from "../../src/common/api/worker/search/DbFacade.js"
Add SQLite search on clients where offline storage is available - Introduce a separate Indexer for SQLite using FTS5 - Split search backends and use the right one based on client (IndexedDB for Browser, and OfflineStorage everywhere else) - Split SearchFacade into two implementations - Adds a table for storing unindexed metadata for mails - Escape special character for SQLite search To escape special characters from fts5 syntax. However, simply surrounding each token in quotes is sufficient to do this. See section 3.1 "FTS5 Strings" here: https://www.sqlite.org/fts5.html which states that a string may be specified by surrounding it in quotes, and that special string requirements only exist for strings that are not in quotes. - Add EncryptedDbWrapper - Simplify out of sync logic in IndexedDbIndexer - Fix deadlock when initializing IndexedDbIndexer - Cleanup indexedDb index when migrating to offline storage index - Pass contactSuggestionFacade to IndexedDbSearchFacade The only suggestion facade used by IndexedDbSearchFacade was the contact suggestion facade. So we made it clearer. - Remove IndexerCore stats - Split custom cache handlers into separate files We were already doing this with user, so we should do this with the other entity types. - Rewrite IndexedDb tests - Add OfflineStorage indexer tests - Add custom cache handlers tests to OfflineStorageTest - Add tests for custom cache handlers with ephemeral storage - Use dbStub instead of dbMock in IndexedDbIndexerTest - Replace spy with testdouble in IndexedDbIndexerTest Close #8550 Co-authored-by: ivk <ivk@tutao.de> Co-authored-by: paw <paw-hub@users.noreply.github.com> Co-authored-by: wrd <wrd@tutao.de> Co-authored-by: bir <bir@tutao.de> Co-authored-by: hrb-hub <hrb-hub@users.noreply.github.com>
2025-03-13 16:37:55 +01:00
import { assertNotNull, clone, deepEqual, defer, Thunk, typedEntries, TypeRef } from "@tutao/tutanota-utils"
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"
import { ScheduledPeriodicId, ScheduledTimeoutId, Scheduler } from "../../src/common/api/common/utils/Scheduler.js"
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, ServerModels, 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"
import { dummyResolver } from "./api/worker/crypto/InstancePipelineTestUtils"
Add SQLite search on clients where offline storage is available - Introduce a separate Indexer for SQLite using FTS5 - Split search backends and use the right one based on client (IndexedDB for Browser, and OfflineStorage everywhere else) - Split SearchFacade into two implementations - Adds a table for storing unindexed metadata for mails - Escape special character for SQLite search To escape special characters from fts5 syntax. However, simply surrounding each token in quotes is sufficient to do this. See section 3.1 "FTS5 Strings" here: https://www.sqlite.org/fts5.html which states that a string may be specified by surrounding it in quotes, and that special string requirements only exist for strings that are not in quotes. - Add EncryptedDbWrapper - Simplify out of sync logic in IndexedDbIndexer - Fix deadlock when initializing IndexedDbIndexer - Cleanup indexedDb index when migrating to offline storage index - Pass contactSuggestionFacade to IndexedDbSearchFacade The only suggestion facade used by IndexedDbSearchFacade was the contact suggestion facade. So we made it clearer. - Remove IndexerCore stats - Split custom cache handlers into separate files We were already doing this with user, so we should do this with the other entity types. - Rewrite IndexedDb tests - Add OfflineStorage indexer tests - Add custom cache handlers tests to OfflineStorageTest - Add tests for custom cache handlers with ephemeral storage - Use dbStub instead of dbMock in IndexedDbIndexerTest - Replace spy with testdouble in IndexedDbIndexerTest Close #8550 Co-authored-by: ivk <ivk@tutao.de> Co-authored-by: paw <paw-hub@users.noreply.github.com> Co-authored-by: wrd <wrd@tutao.de> Co-authored-by: bir <bir@tutao.de> Co-authored-by: hrb-hub <hrb-hub@users.noreply.github.com>
2025-03-13 16:37:55 +01:00
import { EncryptedDbWrapper } from "../../src/common/api/worker/search/EncryptedDbWrapper"
export const browserDataStub: BrowserData = {
needsMicrotaskHack: false,
needsExplicitIDBIds: false,
2022-12-27 15:37:40 +01:00
indexedDbSupported: true,
}
2022-12-27 15:37:40 +01:00
export function makeCore(
args?: {
Add SQLite search on clients where offline storage is available - Introduce a separate Indexer for SQLite using FTS5 - Split search backends and use the right one based on client (IndexedDB for Browser, and OfflineStorage everywhere else) - Split SearchFacade into two implementations - Adds a table for storing unindexed metadata for mails - Escape special character for SQLite search To escape special characters from fts5 syntax. However, simply surrounding each token in quotes is sufficient to do this. See section 3.1 "FTS5 Strings" here: https://www.sqlite.org/fts5.html which states that a string may be specified by surrounding it in quotes, and that special string requirements only exist for strings that are not in quotes. - Add EncryptedDbWrapper - Simplify out of sync logic in IndexedDbIndexer - Fix deadlock when initializing IndexedDbIndexer - Cleanup indexedDb index when migrating to offline storage index - Pass contactSuggestionFacade to IndexedDbSearchFacade The only suggestion facade used by IndexedDbSearchFacade was the contact suggestion facade. So we made it clearer. - Remove IndexerCore stats - Split custom cache handlers into separate files We were already doing this with user, so we should do this with the other entity types. - Rewrite IndexedDb tests - Add OfflineStorage indexer tests - Add custom cache handlers tests to OfflineStorageTest - Add tests for custom cache handlers with ephemeral storage - Use dbStub instead of dbMock in IndexedDbIndexerTest - Replace spy with testdouble in IndexedDbIndexerTest Close #8550 Co-authored-by: ivk <ivk@tutao.de> Co-authored-by: paw <paw-hub@users.noreply.github.com> Co-authored-by: wrd <wrd@tutao.de> Co-authored-by: bir <bir@tutao.de> Co-authored-by: hrb-hub <hrb-hub@users.noreply.github.com>
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
Add SQLite search on clients where offline storage is available - Introduce a separate Indexer for SQLite using FTS5 - Split search backends and use the right one based on client (IndexedDB for Browser, and OfflineStorage everywhere else) - Split SearchFacade into two implementations - Adds a table for storing unindexed metadata for mails - Escape special character for SQLite search To escape special characters from fts5 syntax. However, simply surrounding each token in quotes is sufficient to do this. See section 3.1 "FTS5 Strings" here: https://www.sqlite.org/fts5.html which states that a string may be specified by surrounding it in quotes, and that special string requirements only exist for strings that are not in quotes. - Add EncryptedDbWrapper - Simplify out of sync logic in IndexedDbIndexer - Fix deadlock when initializing IndexedDbIndexer - Cleanup indexedDb index when migrating to offline storage index - Pass contactSuggestionFacade to IndexedDbSearchFacade The only suggestion facade used by IndexedDbSearchFacade was the contact suggestion facade. So we made it clearer. - Remove IndexerCore stats - Split custom cache handlers into separate files We were already doing this with user, so we should do this with the other entity types. - Rewrite IndexedDb tests - Add OfflineStorage indexer tests - Add custom cache handlers tests to OfflineStorageTest - Add tests for custom cache handlers with ephemeral storage - Use dbStub instead of dbMock in IndexedDbIndexerTest - Replace spy with testdouble in IndexedDbIndexerTest Close #8550 Co-authored-by: ivk <ivk@tutao.de> Co-authored-by: paw <paw-hub@users.noreply.github.com> Co-authored-by: wrd <wrd@tutao.de> Co-authored-by: bir <bir@tutao.de> Co-authored-by: hrb-hub <hrb-hub@users.noreply.github.com>
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,
}
Add SQLite search on clients where offline storage is available - Introduce a separate Indexer for SQLite using FTS5 - Split search backends and use the right one based on client (IndexedDB for Browser, and OfflineStorage everywhere else) - Split SearchFacade into two implementations - Adds a table for storing unindexed metadata for mails - Escape special character for SQLite search To escape special characters from fts5 syntax. However, simply surrounding each token in quotes is sufficient to do this. See section 3.1 "FTS5 Strings" here: https://www.sqlite.org/fts5.html which states that a string may be specified by surrounding it in quotes, and that special string requirements only exist for strings that are not in quotes. - Add EncryptedDbWrapper - Simplify out of sync logic in IndexedDbIndexer - Fix deadlock when initializing IndexedDbIndexer - Cleanup indexedDb index when migrating to offline storage index - Pass contactSuggestionFacade to IndexedDbSearchFacade The only suggestion facade used by IndexedDbSearchFacade was the contact suggestion facade. So we made it clearer. - Remove IndexerCore stats - Split custom cache handlers into separate files We were already doing this with user, so we should do this with the other entity types. - Rewrite IndexedDb tests - Add OfflineStorage indexer tests - Add custom cache handlers tests to OfflineStorageTest - Add tests for custom cache handlers with ephemeral storage - Use dbStub instead of dbMock in IndexedDbIndexerTest - Replace spy with testdouble in IndexedDbIndexerTest Close #8550 Co-authored-by: ivk <ivk@tutao.de> Co-authored-by: paw <paw-hub@users.noreply.github.com> Co-authored-by: wrd <wrd@tutao.de> Co-authored-by: bir <bir@tutao.de> Co-authored-by: hrb-hub <hrb-hub@users.noreply.github.com>
2025-03-13 16:37:55 +01:00
const core = new IndexerCore(db, browserData)
if (mocker) mock(core, mocker)
return core
}
2023-11-10 16:26:04 +01:00
export function makeKeyStoreFacade(uint8ArrayKey: Uint8Array): DesktopKeyStoreFacade {
const o: DesktopKeyStoreFacade = object()
when(o.getDeviceKey()).thenResolve(uint8ArrayToKey(uint8ArrayKey))
when(o.getKeyChainKey()).thenResolve(uint8ArrayToKey(uint8ArrayKey))
2023-11-10 16:26:04 +01:00
return o
}
type IdThunk = {
id: ScheduledTimeoutId
thunk: Thunk
}
export class SchedulerMock implements Scheduler {
alarmId: number = 0
/** key is the time */
scheduledAt: Map<number, IdThunk> = new Map()
scheduledAfter: Map<number, IdThunk> = new Map()
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
}
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
}
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 })
return id
}
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)
}
unschedulePeriodic(id: ScheduledPeriodicId) {
this.cancelledPeriodic.add(id)
}
_incAlarmId(): ScheduledTimeoutId {
return this.alarmId++
}
2022-12-27 15:37:40 +01:00
}
export const domainConfigStub: DomainConfig = {
firstPartyDomain: true,
partneredDomainTransitionUrl: "",
apiUrl: "",
u2fAppId: "",
webauthnRpId: "",
referralBaseUrl: "",
giftCardBaseUrl: "",
paymentUrl: "",
webauthnUrl: "",
legacyWebauthnUrl: "",
webauthnMobileUrl: "",
legacyWebauthnMobileUrl: "",
websiteBaseUrl: "",
}
2023-11-20 17:08:24 +01:00
// non-async copy of the function
function resolveTypeReference(typeRef: TypeRef<any>): TypeModel {
const modelMap = ClientModelInfo.getNewInstanceForTestsOnly().typeModels[typeRef.app]
const typeModel = modelMap[typeRef.typeId]
if (typeModel == null) {
throw new Error("Cannot find TypeRef: " + JSON.stringify(typeRef))
} else {
return typeModel
}
}
// 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") {
Add SQLite search on clients where offline storage is available - Introduce a separate Indexer for SQLite using FTS5 - Split search backends and use the right one based on client (IndexedDB for Browser, and OfflineStorage everywhere else) - Split SearchFacade into two implementations - Adds a table for storing unindexed metadata for mails - Escape special character for SQLite search To escape special characters from fts5 syntax. However, simply surrounding each token in quotes is sufficient to do this. See section 3.1 "FTS5 Strings" here: https://www.sqlite.org/fts5.html which states that a string may be specified by surrounding it in quotes, and that special string requirements only exist for strings that are not in quotes. - Add EncryptedDbWrapper - Simplify out of sync logic in IndexedDbIndexer - Fix deadlock when initializing IndexedDbIndexer - Cleanup indexedDb index when migrating to offline storage index - Pass contactSuggestionFacade to IndexedDbSearchFacade The only suggestion facade used by IndexedDbSearchFacade was the contact suggestion facade. So we made it clearer. - Remove IndexerCore stats - Split custom cache handlers into separate files We were already doing this with user, so we should do this with the other entity types. - Rewrite IndexedDb tests - Add OfflineStorage indexer tests - Add custom cache handlers tests to OfflineStorageTest - Add tests for custom cache handlers with ephemeral storage - Use dbStub instead of dbMock in IndexedDbIndexerTest - Replace spy with testdouble in IndexedDbIndexerTest Close #8550 Co-authored-by: ivk <ivk@tutao.de> Co-authored-by: paw <paw-hub@users.noreply.github.com> Co-authored-by: wrd <wrd@tutao.de> Co-authored-by: bir <bir@tutao.de> Co-authored-by: hrb-hub <hrb-hub@users.noreply.github.com>
2025-03-13 16:37:55 +01:00
return `${value.id}_id`
} else if (valueName === "_permissions") {
Add SQLite search on clients where offline storage is available - Introduce a separate Indexer for SQLite using FTS5 - Split search backends and use the right one based on client (IndexedDB for Browser, and OfflineStorage everywhere else) - Split SearchFacade into two implementations - Adds a table for storing unindexed metadata for mails - Escape special character for SQLite search To escape special characters from fts5 syntax. However, simply surrounding each token in quotes is sufficient to do this. See section 3.1 "FTS5 Strings" here: https://www.sqlite.org/fts5.html which states that a string may be specified by surrounding it in quotes, and that special string requirements only exist for strings that are not in quotes. - Add EncryptedDbWrapper - Simplify out of sync logic in IndexedDbIndexer - Fix deadlock when initializing IndexedDbIndexer - Cleanup indexedDb index when migrating to offline storage index - Pass contactSuggestionFacade to IndexedDbSearchFacade The only suggestion facade used by IndexedDbSearchFacade was the contact suggestion facade. So we made it clearer. - Remove IndexerCore stats - Split custom cache handlers into separate files We were already doing this with user, so we should do this with the other entity types. - Rewrite IndexedDb tests - Add OfflineStorage indexer tests - Add custom cache handlers tests to OfflineStorageTest - Add tests for custom cache handlers with ephemeral storage - Use dbStub instead of dbMock in IndexedDbIndexerTest - Replace spy with testdouble in IndexedDbIndexerTest Close #8550 Co-authored-by: ivk <ivk@tutao.de> Co-authored-by: paw <paw-hub@users.noreply.github.com> Co-authored-by: wrd <wrd@tutao.de> Co-authored-by: bir <bir@tutao.de> Co-authored-by: hrb-hub <hrb-hub@users.noreply.github.com>
2025-03-13 16:37:55 +01:00
return `${value.id}_permissions`
} 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:
Add SQLite search on clients where offline storage is available - Introduce a separate Indexer for SQLite using FTS5 - Split search backends and use the right one based on client (IndexedDB for Browser, and OfflineStorage everywhere else) - Split SearchFacade into two implementations - Adds a table for storing unindexed metadata for mails - Escape special character for SQLite search To escape special characters from fts5 syntax. However, simply surrounding each token in quotes is sufficient to do this. See section 3.1 "FTS5 Strings" here: https://www.sqlite.org/fts5.html which states that a string may be specified by surrounding it in quotes, and that special string requirements only exist for strings that are not in quotes. - Add EncryptedDbWrapper - Simplify out of sync logic in IndexedDbIndexer - Fix deadlock when initializing IndexedDbIndexer - Cleanup indexedDb index when migrating to offline storage index - Pass contactSuggestionFacade to IndexedDbSearchFacade The only suggestion facade used by IndexedDbSearchFacade was the contact suggestion facade. So we made it clearer. - Remove IndexerCore stats - Split custom cache handlers into separate files We were already doing this with user, so we should do this with the other entity types. - Rewrite IndexedDb tests - Add OfflineStorage indexer tests - Add custom cache handlers tests to OfflineStorageTest - Add tests for custom cache handlers with ephemeral storage - Use dbStub instead of dbMock in IndexedDbIndexerTest - Replace spy with testdouble in IndexedDbIndexerTest Close #8550 Co-authored-by: ivk <ivk@tutao.de> Co-authored-by: paw <paw-hub@users.noreply.github.com> Co-authored-by: wrd <wrd@tutao.de> Co-authored-by: bir <bir@tutao.de> Co-authored-by: hrb-hub <hrb-hub@users.noreply.github.com>
2025-03-13 16:37:55 +01:00
return `${value.id}_${valueName}`
}
}
}
Add SQLite search on clients where offline storage is available - Introduce a separate Indexer for SQLite using FTS5 - Split search backends and use the right one based on client (IndexedDB for Browser, and OfflineStorage everywhere else) - Split SearchFacade into two implementations - Adds a table for storing unindexed metadata for mails - Escape special character for SQLite search To escape special characters from fts5 syntax. However, simply surrounding each token in quotes is sufficient to do this. See section 3.1 "FTS5 Strings" here: https://www.sqlite.org/fts5.html which states that a string may be specified by surrounding it in quotes, and that special string requirements only exist for strings that are not in quotes. - Add EncryptedDbWrapper - Simplify out of sync logic in IndexedDbIndexer - Fix deadlock when initializing IndexedDbIndexer - Cleanup indexedDb index when migrating to offline storage index - Pass contactSuggestionFacade to IndexedDbSearchFacade The only suggestion facade used by IndexedDbSearchFacade was the contact suggestion facade. So we made it clearer. - Remove IndexerCore stats - Split custom cache handlers into separate files We were already doing this with user, so we should do this with the other entity types. - Rewrite IndexedDb tests - Add OfflineStorage indexer tests - Add custom cache handlers tests to OfflineStorageTest - Add tests for custom cache handlers with ephemeral storage - Use dbStub instead of dbMock in IndexedDbIndexerTest - Replace spy with testdouble in IndexedDbIndexerTest Close #8550 Co-authored-by: ivk <ivk@tutao.de> Co-authored-by: paw <paw-hub@users.noreply.github.com> Co-authored-by: wrd <wrd@tutao.de> Co-authored-by: bir <bir@tutao.de> Co-authored-by: hrb-hub <hrb-hub@users.noreply.github.com>
2025-03-13 16:37:55 +01:00
export function createTestEntity<T extends Entity>(
typeRef: TypeRef<T>,
values?: Partial<T>,
opts?: {
populateAggregates: boolean
},
): T {
const typeModel = resolveTypeReference(typeRef as TypeRef<any>)
const entity = create(typeModel, typeRef, getDefaultTestValue)
Add SQLite search on clients where offline storage is available - Introduce a separate Indexer for SQLite using FTS5 - Split search backends and use the right one based on client (IndexedDB for Browser, and OfflineStorage everywhere else) - Split SearchFacade into two implementations - Adds a table for storing unindexed metadata for mails - Escape special character for SQLite search To escape special characters from fts5 syntax. However, simply surrounding each token in quotes is sufficient to do this. See section 3.1 "FTS5 Strings" here: https://www.sqlite.org/fts5.html which states that a string may be specified by surrounding it in quotes, and that special string requirements only exist for strings that are not in quotes. - Add EncryptedDbWrapper - Simplify out of sync logic in IndexedDbIndexer - Fix deadlock when initializing IndexedDbIndexer - Cleanup indexedDb index when migrating to offline storage index - Pass contactSuggestionFacade to IndexedDbSearchFacade The only suggestion facade used by IndexedDbSearchFacade was the contact suggestion facade. So we made it clearer. - Remove IndexerCore stats - Split custom cache handlers into separate files We were already doing this with user, so we should do this with the other entity types. - Rewrite IndexedDb tests - Add OfflineStorage indexer tests - Add custom cache handlers tests to OfflineStorageTest - Add tests for custom cache handlers with ephemeral storage - Use dbStub instead of dbMock in IndexedDbIndexerTest - Replace spy with testdouble in IndexedDbIndexerTest Close #8550 Co-authored-by: ivk <ivk@tutao.de> Co-authored-by: paw <paw-hub@users.noreply.github.com> Co-authored-by: wrd <wrd@tutao.de> Co-authored-by: bir <bir@tutao.de> Co-authored-by: hrb-hub <hrb-hub@users.noreply.github.com>
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
}
}
}
}
if (values) {
return Object.assign(entity, values)
} else {
return entity
}
}
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
}
}
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}"` })
}
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))}`,
}
}
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
}
export function clientModelAsServerModel(clientModel: ClientModelInfo): ServerModelInfo {
let models = Object.keys(clientModel.typeModels).reduce((obj, app) => {
Object.assign(obj, {
[app]: {
name: app,
version: clientModel.modelInfos[app].version,
types: clone(clientModel.typeModels[app]),
},
})
return obj
}, {}) as ServerModels
const modelInfo = ServerModelInfo.getUninitializedInstanceForTestsOnly(clientModel, () => {
throw new Error("should not fetch in test")
})
modelInfo.typeModels = models
return modelInfo
}
export function clientInitializedTypeModelResolver(): TypeModelResolver {
const clientModelInfo = ClientModelInfo.getNewInstanceForTestsOnly()
const serverModelInfo = clientModelAsServerModel(clientModelInfo)
return new TypeModelResolver(clientModelInfo, serverModelInfo)
}
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]
}
}
}