tutanota/test/tests/api/worker/offline/OfflineStorageTest.ts

1590 lines
61 KiB
TypeScript
Raw Normal View History

2023-06-29 18:26:45 +02:00
import o from "@tutao/otest"
2022-12-27 15:37:40 +01:00
import { verify } from "@tutao/tutanota-test-utils"
import { customTypeEncoders, ensureBase64Ext, OfflineStorage, OfflineStorageCleaner } from "../../../../../src/common/api/worker/offline/OfflineStorage.js"
2022-12-27 15:37:40 +01:00
import { instance, object, when } from "testdouble"
import * as cborg from "cborg"
import {
constructMailSetEntryId,
CUSTOM_MAX_ID,
CUSTOM_MIN_ID,
deconstructMailSetEntryId,
elementIdPart,
GENERATED_MAX_ID,
GENERATED_MIN_ID,
generatedIdToTimestamp,
getElementId,
listIdPart,
timestampToGeneratedId,
} from "../../../../../src/common/api/common/utils/EntityUtils.js"
import { assertNotNull, downcast, getDayShifted, getFirstOrThrow, getTypeId, lastThrow, mapNullable, promiseMap, TypeRef } from "@tutao/tutanota-utils"
import { DateProvider } from "../../../../../src/common/api/common/DateProvider.js"
import {
BodyTypeRef,
createMailFolderRef,
FileTypeRef,
Mail,
MailAddressTypeRef,
MailBagTypeRef,
MailBoxTypeRef,
MailDetailsBlob,
MailDetailsBlobTypeRef,
MailDetailsTypeRef,
MailFolder,
MailFolderTypeRef,
MailSetEntry,
MailSetEntryTypeRef,
2022-12-27 15:37:40 +01:00
MailTypeRef,
RecipientsTypeRef,
} from "../../../../../src/common/api/entities/tutanota/TypeRefs.js"
import { OfflineStorageMigrator } from "../../../../../src/common/api/worker/offline/OfflineStorageMigrator.js"
import { InterWindowEventFacadeSendDispatcher } from "../../../../../src/common/native/common/generatedipc/InterWindowEventFacadeSendDispatcher.js"
import * as fs from "node:fs"
import { untagSqlObject } from "../../../../../src/common/api/worker/offline/SqlValue.js"
import { MailSetKind } from "../../../../../src/common/api/common/TutanotaConstants.js"
import { 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 { createTestEntity } 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"
function incrementId(id: Id, ms: number) {
const timestamp = generatedIdToTimestamp(id)
return timestampToGeneratedId(timestamp + ms)
}
class IdGenerator {
2022-12-27 15:37:40 +01:00
constructor(private currentId: Id) {}
getNext(incrementByMs: number = 60000): Id {
this.currentId = incrementId(this.currentId, incrementByMs)
return this.currentId
}
}
function incrementMailSetEntryId(mailSetEntryId, mailId, ms: number) {
const { receiveDate } = deconstructMailSetEntryId(mailSetEntryId)
return constructMailSetEntryId(new Date(receiveDate.getTime() + ms), mailId)
}
class MailSetEntryIdGenerator {
constructor(private currentMailSetEntryId: Id) {}
getNext(mailId: Id, incrementByMs: number = 60000) {
this.currentMailSetEntryId = incrementMailSetEntryId(this.currentMailSetEntryId, mailId, incrementByMs)
return this.currentMailSetEntryId
}
}
function encode(thing) {
2022-12-27 15:37:40 +01:00
return cborg.encode(thing, { typeEncoders: customTypeEncoders })
}
const nativePath = __NODE_GYP_better_sqlite3
const database = "./testdatabase.sqlite"
export const offlineDatabaseTestKey = Uint8Array.from([3957386659, 354339016, 3786337319, 3366334248])
/**
* remove fields from mails that are added by the model mapper in order to allow comparing mails
*/
function cleanFieldsFromMail(mail: Mail) {
downcast(mail.sender)._id = null
delete downcast(mail)._finalIvs
delete downcast(mail.sender)._finalIvs
return mail
}
/**
* remove fields from mailDetailsBlobs that are added by the model mapper in order to allow comparing mailDetailsBlobs
*/
function clearFieldsFromMailDetailsBlob(mailDetailsBlob: MailDetailsBlob) {
delete downcast(mailDetailsBlob)._finalIvs
downcast(mailDetailsBlob).details._id = null
delete downcast(mailDetailsBlob).details._finalIvs
downcast(mailDetailsBlob).details.body._id = null
delete downcast(mailDetailsBlob).details.body._finalIvs
downcast(mailDetailsBlob).details.recipients._id = null
delete downcast(mailDetailsBlob).details.recipients._finalIvs
return mailDetailsBlob
}
o.spec("OfflineStorageDb", function () {
const now = new Date("2022-01-01 00:00:00 UTC")
const timeRangeDays = 10
const userId = "userId"
const databaseKey = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7])
/** get an id based on a timestamp that is {@param days} days away from the time range cutoff */
2022-12-27 15:37:40 +01:00
const offsetId = (days) => timestampToGeneratedId(getDayShifted(now, 0 - timeRangeDays + days).getTime())
const offsetMailSetEntryId = (days, mailId) => constructMailSetEntryId(getDayShifted(now, 0 - timeRangeDays + days), mailId)
const cuttoffMailSetEntryId = offsetMailSetEntryId(0, GENERATED_MAX_ID)
let dbFacade: DesktopSqlCipher
let dateProviderMock: DateProvider
let storage: OfflineStorage
let migratorMock: OfflineStorageMigrator
let offlineStorageCleanerMock: OfflineStorageCleaner
let interWindowEventSenderMock: InterWindowEventFacadeSendDispatcher
let modelMapper: ModelMapper
o.beforeEach(async function () {
dbFacade = new DesktopSqlCipher(nativePath, database, false)
dateProviderMock = object<DateProvider>()
migratorMock = instance(OfflineStorageMigrator)
interWindowEventSenderMock = instance(InterWindowEventFacadeSendDispatcher)
offlineStorageCleanerMock = new MailOfflineCleaner()
modelMapper = new ModelMapper(resolveClientTypeReference, resolveServerTypeReference)
when(dateProviderMock.now()).thenReturn(now.getTime())
storage = new OfflineStorage(dbFacade, interWindowEventSenderMock, dateProviderMock, migratorMock, offlineStorageCleanerMock, modelMapper)
})
o.afterEach(async function () {
await dbFacade.closeDb()
await fs.promises.unlink(database)
})
o.spec("Unit test", function () {
async function getAllIdsForType(typeRef: TypeRef<unknown>): Promise<Id[]> {
const typeModel = await resolveClientTypeReference(typeRef)
let preparedQuery
switch (typeModel.type) {
case TypeId.Element.valueOf():
preparedQuery = sql`select *
from element_entities
where type = ${getTypeId(typeRef)}`
break
case TypeId.ListElement.valueOf():
preparedQuery = sql`select *
from list_entities
where type = ${getTypeId(typeRef)}`
break
case TypeId.BlobElement.valueOf():
preparedQuery = sql`select *
from blob_element_entities
where type = ${getTypeId(typeRef)}`
break
default:
throw new Error("must be a persistent type")
}
return (await dbFacade.all(preparedQuery.query, preparedQuery.params)).map((r) => r.elementId.value as Id)
}
o("migrations are run", async function () {
2022-12-27 15:37:40 +01:00
await storage.init({ userId, databaseKey, timeRangeDays, forceNewDatabase: false })
verify(migratorMock.migrate(storage, dbFacade))
})
o.spec("Offline storage round trip", function () {
o.spec("ElementType", function () {
o("deleteAllOfType", async function () {
const userId = "id1"
const storableUser = createTestEntity(UserTypeRef, {
_id: userId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
userGroup: createTestEntity(GroupMembershipTypeRef, {
group: "groupId",
groupInfo: ["groupInfoListId", "groupInfoElementId"],
groupMember: ["groupMemberListId", "groupMemberElementId"],
}),
successfulLogins: "successfulLogins",
failedLogins: "failedLogins",
secondFactorAuthentications: "secondFactorAuthentications",
})
await storage.init({ userId, databaseKey, timeRangeDays, forceNewDatabase: false })
let user = await storage.get(UserTypeRef, null, userId)
o(user).equals(null)
await storage.put(storableUser)
user = await storage.get(UserTypeRef, null, userId)
o(user!._id).equals(storableUser._id)
await storage.deleteAllOfType(UserTypeRef)
user = await storage.get(UserTypeRef, null, userId)
o(user).equals(null)
})
})
o.spec("ListElementType generatedId", function () {
o("deleteAllOfType", async function () {
const listId = "listId1"
const elementId = "id1"
const storableMail = createTestEntity(MailTypeRef, {
_id: [listId, elementId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
await storage.init({ userId: elementId, databaseKey, timeRangeDays, forceNewDatabase: false })
let mail = await storage.get(MailTypeRef, listId, elementId)
o(mail).equals(null)
await storage.put(storableMail)
await storage.setNewRangeForList(MailTypeRef, listId, elementId, elementId)
mail = await storage.get(MailTypeRef, listId, elementId)
o(mail!._id).deepEquals(storableMail._id)
const rangeBefore = await storage.getRangeForList(MailTypeRef, listId)
o(rangeBefore).deepEquals({ upper: elementId, lower: elementId })
await storage.deleteAllOfType(MailTypeRef)
mail = await storage.get(MailTypeRef, listId, elementId)
o(mail).equals(null)
const rangeAfter = await storage.getRangeForList(MailTypeRef, listId)
o(rangeAfter).equals(null)
})
o("deleteWholeList", async function () {
const listOne = "listId1"
const listTwo = "listId2"
await storage.init({ userId: "user", databaseKey, timeRangeDays, forceNewDatabase: false })
const listOneMailOne = createTestEntity(MailTypeRef, {
_id: [listOne, "id1"],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const listOneMailTwo = createTestEntity(MailTypeRef, {
_id: [listOne, "id2"],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const listTwoMail = createTestEntity(MailTypeRef, {
_id: [listTwo, "id3"],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
await storage.put(listOneMailOne)
await storage.put(listOneMailTwo)
await storage.put(listTwoMail)
await storage.setNewRangeForList(MailTypeRef, listOne, "id1", "id2")
await storage.setNewRangeForList(MailTypeRef, listTwo, "id3", "id3")
await storage.deleteWholeList(MailTypeRef, listOne)
const mailsInListOne = await storage.getWholeList(MailTypeRef, listOne)
const mailsInListTwo = await storage.getWholeList(MailTypeRef, listTwo)
cleanFieldsFromMail(mailsInListTwo[0])
const rangeListOne = await storage.getRangeForList(MailTypeRef, listOne)
const rangeListTwo = await storage.getRangeForList(MailTypeRef, listTwo)
o(mailsInListOne).deepEquals([])
o(mailsInListTwo).deepEquals([listTwoMail])
o(rangeListOne).equals(null)
o(rangeListTwo).deepEquals({ lower: "id3", upper: "id3" })
})
o("provideMultiple", async function () {
const listId = "listId1"
const elementId1 = "id1"
const elementId2 = "id2"
const storableMail1 = createTestEntity(MailTypeRef, {
_id: [listId, elementId1],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const storableMail2 = createTestEntity(MailTypeRef, {
_id: [listId, elementId2],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
await storage.init({ userId: elementId1, databaseKey, timeRangeDays, forceNewDatabase: false })
let mails = await storage.provideMultiple(MailTypeRef, listId, [elementId1])
o(mails).deepEquals([])
await storage.put(storableMail1)
mails = await storage.provideMultiple(MailTypeRef, listId, [elementId1, elementId2])
cleanFieldsFromMail(mails[0])
o(mails).deepEquals([storableMail1])
await storage.put(storableMail2)
mails = await storage.provideMultiple(MailTypeRef, listId, [elementId1, elementId2])
cleanFieldsFromMail(mails[0])
cleanFieldsFromMail(mails[1])
o(mails).deepEquals([storableMail1, storableMail2])
})
})
o.spec("ListElementType customId", function () {
o("deleteAllOfType", async function () {
const listId = "listId1"
const elementId = constructMailSetEntryId(new Date(), "mailId")
const storableMailSetEntry = createTestEntity(MailSetEntryTypeRef, {
_id: [listId, elementId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: ["mailListId", "mailId"],
})
await storage.init({ userId: elementId, databaseKey, timeRangeDays, forceNewDatabase: false })
let mailSetEntry = await storage.get(MailSetEntryTypeRef, listId, elementId)
o(mailSetEntry).equals(null)
await storage.put(storableMailSetEntry)
await storage.setNewRangeForList(MailSetEntryTypeRef, listId, elementId, elementId)
mailSetEntry = await storage.get(MailSetEntryTypeRef, listId, elementId)
o(mailSetEntry!._id).deepEquals(storableMailSetEntry._id)
const rangeBefore = await storage.getRangeForList(MailSetEntryTypeRef, listId)
o(rangeBefore).deepEquals({ upper: elementId, lower: elementId })
await storage.deleteAllOfType(MailSetEntryTypeRef)
mailSetEntry = await storage.get(MailSetEntryTypeRef, listId, elementId)
o(mailSetEntry).equals(null)
const rangeAfter = await storage.getRangeForList(MailSetEntryTypeRef, listId)
o(rangeAfter).equals(null)
})
o("provideMultiple", async function () {
const listId = "listId1"
const elementId1 = constructMailSetEntryId(new Date(1724675875113), "mailId1")
const elementId2 = constructMailSetEntryId(new Date(1724675899978), "mailId2")
const storableMailSetEntry1 = createTestEntity(MailSetEntryTypeRef, {
_id: [listId, elementId1],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: ["mailListId", "mailId"],
})
const storableMailSetEntry2 = createTestEntity(MailSetEntryTypeRef, {
_id: [listId, elementId2],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: ["mailListId", "mailId"],
})
await storage.init({ userId: elementId1, databaseKey, timeRangeDays, forceNewDatabase: false })
let mails = await storage.provideMultiple(MailSetEntryTypeRef, listId, [elementId1])
o(mails).deepEquals([])
await storage.put(storableMailSetEntry1)
mails = await storage.provideMultiple(MailSetEntryTypeRef, listId, [elementId1, elementId2])
delete downcast(mails[0])._finalIvs
o(mails).deepEquals([storableMailSetEntry1])
await storage.put(storableMailSetEntry2)
mails = await storage.provideMultiple(MailSetEntryTypeRef, listId, [elementId1, elementId2])
delete downcast(mails[0])._finalIvs
delete downcast(mails[1])._finalIvs
o(mails).deepEquals([storableMailSetEntry1, storableMailSetEntry2])
})
})
o.spec("BlobElementType", function () {
o("put, get and delete", async function () {
const archiveId = "archiveId"
const blobElementId = "id1"
const storableMailDetails = createTestEntity(MailDetailsBlobTypeRef, {
_id: [archiveId, blobElementId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
})
await storage.init({ userId, databaseKey, timeRangeDays, forceNewDatabase: false })
let mailDetailsBlob = await storage.get(MailDetailsBlobTypeRef, archiveId, blobElementId)
o(mailDetailsBlob).equals(null)
await storage.put(storableMailDetails)
mailDetailsBlob = await storage.get(MailDetailsBlobTypeRef, archiveId, blobElementId)
clearFieldsFromMailDetailsBlob(assertNotNull(mailDetailsBlob))
o(mailDetailsBlob).deepEquals(storableMailDetails)
await storage.deleteIfExists(MailDetailsBlobTypeRef, archiveId, blobElementId)
mailDetailsBlob = await storage.get(MailDetailsBlobTypeRef, archiveId, blobElementId)
o(mailDetailsBlob).equals(null)
})
o("put, get and deleteAllOwnedBy", async function () {
const archiveId = "archiveId"
const blobElementId = "id1"
const _ownerGroup = "ownerGroup"
const storableMailDetails = createTestEntity(MailDetailsBlobTypeRef, {
_id: [archiveId, blobElementId],
_ownerGroup,
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
})
await storage.init({ userId, databaseKey, timeRangeDays, forceNewDatabase: false })
await storage.put(storableMailDetails)
await storage.deleteAllOwnedBy(_ownerGroup)
const mailDetailsBlob = await storage.get(MailDetailsBlobTypeRef, archiveId, blobElementId)
o(mailDetailsBlob).equals(null)
})
})
})
o.spec("Clearing excluded data for MailSet mailbox", function () {
const spamFolderId = "spamFolder"
const trashFolderId = "trashFolder"
const spamFolderEntriesId = "spamFolderEntriesId"
const trashFolderEntriesId = "trashFolderEntriesId"
const mailBagMailListId = "mailBagMailListId"
const mailSetEntryType = getTypeId(MailSetEntryTypeRef)
o.beforeEach(async function () {
await storage.init({ userId, databaseKey, timeRangeDays, forceNewDatabase: false })
await storage.put(
createTestEntity(MailBoxTypeRef, {
_id: "mailboxId",
_ownerGroup: "ownerGroup",
_permissions: "permissions",
sentAttachments: "sentAttachments",
receivedAttachments: "receivedAttachments",
importedAttachments: "importedAttachments",
mailImportStates: "mailImportStates",
currentMailBag: createTestEntity(MailBagTypeRef, { _id: "mailBagId", mails: mailBagMailListId }),
folders: createMailFolderRef({ folders: "mailFolderList" }),
}),
)
await storage.put(
createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", spamFolderId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
entries: spamFolderEntriesId,
folderType: MailSetKind.SPAM,
}),
)
await storage.put(
createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", trashFolderId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
entries: trashFolderEntriesId,
folderType: MailSetKind.TRASH,
}),
)
})
o("ranges before timeRangeDays will be deleted", async function () {
const oneDayBeforeTimeRangeDays = -1
const twoDaysBeforeTimeRangeDays = -2
const mailId: IdTuple = [mailBagMailListId, "anything"]
const mailSetEntryElementId = offsetMailSetEntryId(oneDayBeforeTimeRangeDays, elementIdPart(mailId))
const mailSetEntryId: IdTuple = ["mailSetEntriesListId", mailSetEntryElementId]
const mailDetailsBlobId: IdTuple = ["mailDetailsList", "mailDetailsBlobId"]
await storage.put(
createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", "mailFolderId"],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
entries: listIdPart(mailSetEntryId),
}),
)
await storage.put(
createTestEntity(MailSetEntryTypeRef, {
_id: mailSetEntryId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: mailId,
}),
)
await storage.put(
createTestEntity(MailTypeRef, {
_id: mailId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: mailDetailsBlobId,
sets: [mailSetEntryId],
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
}),
)
await storage.put(
createTestEntity(MailDetailsBlobTypeRef, {
_id: mailDetailsBlobId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
}),
)
const lowerMailSetEntryIdForRange = offsetMailSetEntryId(twoDaysBeforeTimeRangeDays, GENERATED_MIN_ID)
const upperMailSetEntryIdForRange = offsetMailSetEntryId(oneDayBeforeTimeRangeDays, GENERATED_MAX_ID)
await storage.setNewRangeForList(MailSetEntryTypeRef, listIdPart(mailSetEntryId), lowerMailSetEntryIdForRange, upperMailSetEntryIdForRange)
const upperBeforeTimeRangeDays = offsetId(oneDayBeforeTimeRangeDays) // negative number == mail newer than timeRangeDays
const lowerBeforeTimeRangeDays = offsetId(twoDaysBeforeTimeRangeDays)
await storage.setNewRangeForList(MailTypeRef, mailBagMailListId, lowerBeforeTimeRangeDays, upperBeforeTimeRangeDays)
// Here we clear the excluded data
await storage.clearExcludedData(timeRangeDays, userId)
const allRanges = await dbFacade.all("SELECT * FROM ranges", [])
o(allRanges).deepEquals([])
const allMails = await getAllIdsForType(MailTypeRef)
o(allMails).deepEquals([])
const allMailSetEntries = await getAllIdsForType(MailSetEntryTypeRef)
o(allMailSetEntries).deepEquals([])
const allBlobDetails = await getAllIdsForType(MailDetailsBlobTypeRef)
o(allBlobDetails).deepEquals([])
})
o("modified ranges will be shrunk", async function () {
const twoDaysBeforeTimeRangeDays = -2
const twoDaysAfterTimeRangeDays = 2
const entriesListId = "mailSetEntriesListIdRanges"
const lowerMailSetEntryIdForRange = offsetMailSetEntryId(twoDaysBeforeTimeRangeDays, GENERATED_MIN_ID)
const upperMailSetEntryIdForRange = offsetMailSetEntryId(twoDaysAfterTimeRangeDays, GENERATED_MAX_ID)
await storage.put(
createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", "mailFolderId"],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
entries: entriesListId,
folderType: MailSetKind.INBOX,
}),
)
await storage.setNewRangeForList(MailSetEntryTypeRef, entriesListId, lowerMailSetEntryIdForRange, upperMailSetEntryIdForRange)
// Here we clear the excluded data
await storage.clearExcludedData(timeRangeDays, userId)
const newRange = await dbFacade.get("select * from ranges", [])
const mailSetEntryTypeModel = await resolveClientTypeReference(MailSetEntryTypeRef)
o(mapNullable(newRange, untagSqlObject)).deepEquals({
type: mailSetEntryType,
listId: entriesListId,
// we need to encode with base64Ext, as we read raw data from the database, which stores custom elementIds in base64Ext not base64Url
lower: ensureBase64Ext(mailSetEntryTypeModel, cuttoffMailSetEntryId),
upper: ensureBase64Ext(mailSetEntryTypeModel, upperMailSetEntryIdForRange),
})
})
o("unmodified ranges will not be deleted or shrunk", async function () {
const oneDayAfterTimeRangeDays = 1
const twoDaysAfterTimeRangeDays = 2
const entriesListId = "mailSetEntriesListIdRanges"
const lowerMailSetEntryIdForRange = offsetMailSetEntryId(oneDayAfterTimeRangeDays, GENERATED_MIN_ID)
const upperMailSetEntryIdForRange = offsetMailSetEntryId(twoDaysAfterTimeRangeDays, GENERATED_MAX_ID)
await storage.put(
createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", "mailFolderId"],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
entries: entriesListId,
folderType: MailSetKind.CUSTOM,
}),
)
await storage.setNewRangeForList(MailSetEntryTypeRef, entriesListId, lowerMailSetEntryIdForRange, upperMailSetEntryIdForRange)
// Here we clear the excluded data
await storage.clearExcludedData(timeRangeDays, userId)
const newRange = await dbFacade.get("select * from ranges", [])
const mailSetEntryTypeModel = await resolveClientTypeReference(MailSetEntryTypeRef)
o(mapNullable(newRange, untagSqlObject)).deepEquals({
type: mailSetEntryType,
listId: entriesListId,
// we need to encode with base64Ext, as we read raw data from the database, which stores custom elementIds in base64Ext not base64Url
lower: ensureBase64Ext(mailSetEntryTypeModel, lowerMailSetEntryIdForRange),
upper: ensureBase64Ext(mailSetEntryTypeModel, upperMailSetEntryIdForRange),
})
})
o("complete ranges won't be lost if entities are all newer than cutoff", async function () {
const twoDaysAfterTimeRangeDays = 2
const mailId: IdTuple = [mailBagMailListId, offsetId(twoDaysAfterTimeRangeDays)]
const mailSetEntryElementId = offsetMailSetEntryId(twoDaysAfterTimeRangeDays, elementIdPart(mailId))
const mailSetEntryId: IdTuple = ["mailSetEntriesListId", mailSetEntryElementId]
const mailDetailsBlobId: IdTuple = ["mailDetailsList", "mailDetailsBlobId"]
const lowerMailSetEntryIdForRange = CUSTOM_MIN_ID
const upperMailSetEntryIdForRange = CUSTOM_MAX_ID
await storage.setNewRangeForList(MailSetEntryTypeRef, listIdPart(mailSetEntryId), lowerMailSetEntryIdForRange, upperMailSetEntryIdForRange)
const upper = offsetId(twoDaysAfterTimeRangeDays)
const lower = GENERATED_MIN_ID
await storage.setNewRangeForList(MailTypeRef, mailBagMailListId, lower, upper)
const mail = createTestEntity(MailTypeRef, {
_id: mailId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: mailDetailsBlobId,
sets: [mailSetEntryId],
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const mailFolder = createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", "folderId"],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
entries: listIdPart(mailSetEntryId),
})
await storage.put(mailFolder)
await storage.put(mail)
await storage.put(
createTestEntity(MailSetEntryTypeRef, {
_id: mailSetEntryId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: mailId,
}),
)
await storage.put(
createTestEntity(MailDetailsBlobTypeRef, {
_id: mailDetailsBlobId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
}),
)
// Here we clear the excluded data
await storage.clearExcludedData(timeRangeDays, userId)
const newRange = await dbFacade.get("select * from ranges", [])
const mailSetEntryTypeModel = await resolveClientTypeReference(MailSetEntryTypeRef)
o(mapNullable(newRange, untagSqlObject)).deepEquals({
type: mailSetEntryType,
listId: listIdPart(mailSetEntryId),
// we need to encode with base64Ext, as we read raw data from the database, which stores custom elementIds in base64Ext not base64Url
lower: ensureBase64Ext(mailSetEntryTypeModel, lowerMailSetEntryIdForRange),
upper: ensureBase64Ext(mailSetEntryTypeModel, upperMailSetEntryIdForRange),
})
const allFolderIds = await getAllIdsForType(MailFolderTypeRef)
o(allFolderIds).deepEquals(["folderId", spamFolderId, trashFolderId])
const allMailIds = await getAllIdsForType(MailTypeRef)
o(allMailIds).deepEquals([elementIdPart(mailId)])
const allMailSetEntries = await getAllIdsForType(MailSetEntryTypeRef)
// we need to encode with base64Ext, as we read raw data from the database, which stores custom elementIds in base64Ext not base64Url
o(allMailSetEntries).deepEquals([ensureBase64Ext(mailSetEntryTypeModel, mailSetEntryElementId)])
const allBlobDetails = await getAllIdsForType(MailDetailsBlobTypeRef)
o(allBlobDetails).deepEquals([elementIdPart(mailDetailsBlobId)])
})
o("complete ranges will be modified if some entities are older than cutoff", async function () {
const twoDaysBeforeTimeRangeDays = -2
const mailId: IdTuple = [mailBagMailListId, offsetId(twoDaysBeforeTimeRangeDays)]
const mailSetEntryElementId = offsetMailSetEntryId(twoDaysBeforeTimeRangeDays, elementIdPart(mailId))
const mailSetEntryId: IdTuple = ["mailSetEntriesListId", mailSetEntryElementId]
const mailDetailsBlobId: IdTuple = ["mailDetailsList", "mailDetailsBlobId"]
const lowerMailSetEntryIdForRange = CUSTOM_MIN_ID
const upperMailSetEntryIdForRange = CUSTOM_MAX_ID
await storage.setNewRangeForList(MailSetEntryTypeRef, listIdPart(mailSetEntryId), lowerMailSetEntryIdForRange, upperMailSetEntryIdForRange)
const upper = offsetId(twoDaysBeforeTimeRangeDays)
const lower = GENERATED_MIN_ID
await storage.setNewRangeForList(MailTypeRef, mailBagMailListId, lower, upper)
const mail = createTestEntity(MailTypeRef, {
_id: mailId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: mailDetailsBlobId,
sets: [mailSetEntryId],
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const mailFolder = createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", "folderId"],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
entries: listIdPart(mailSetEntryId),
})
await storage.put(mailFolder)
await storage.put(mail)
await storage.put(
createTestEntity(MailSetEntryTypeRef, {
_id: mailSetEntryId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: mailId,
}),
)
await storage.put(
createTestEntity(MailDetailsBlobTypeRef, {
_id: mailDetailsBlobId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
}),
)
// Here we clear the excluded data
await storage.clearExcludedData(timeRangeDays, userId)
const newRange = await dbFacade.get("select * from ranges", [])
const mailSetEntryTypeModel = await resolveClientTypeReference(MailSetEntryTypeRef)
o(mapNullable(newRange, untagSqlObject)).deepEquals({
type: mailSetEntryType,
listId: listIdPart(mailSetEntryId),
// we need to encode with base64Ext, as we read raw data from the database, which stores custom elementIds in base64Ext not base64Url
lower: ensureBase64Ext(mailSetEntryTypeModel, cuttoffMailSetEntryId),
upper: ensureBase64Ext(mailSetEntryTypeModel, upperMailSetEntryIdForRange),
})
const allFolderIds = await getAllIdsForType(MailFolderTypeRef)
o(allFolderIds).deepEquals(["folderId", spamFolderId, trashFolderId])
const allMailIds = await getAllIdsForType(MailTypeRef)
o(allMailIds).deepEquals([])
const allMailSetEntries = await getAllIdsForType(MailSetEntryTypeRef)
// we need to encode with base64Ext, as we read raw data from the database, which stores custom elementIds in base64Ext not base64Url
o(allMailSetEntries).deepEquals([])
const allBlobDetails = await getAllIdsForType(MailDetailsBlobTypeRef)
o(allBlobDetails).deepEquals([])
})
o("trash and spam descendants are cleared but mails are only if before timeRangeDays", async function () {
const twoDaysAfterTimeRangeDays = 2
const threeDaysAfterTimeRangeDays = 3
const fourDaysBeforeTimeRangeDays = -4
const spamDetailsId: IdTuple = ["detailsListId", "spamDetailsId"]
const trashDetailsId: IdTuple = ["detailsListId", "trashDetailsId"]
const trashSubfolderDetailsId: IdTuple = ["detailsListId", "trashSubFolderDetailsId"]
const trashSubfolderId = "trashSubfolderId"
const trashSubfolderEntriesId = "trashSubfolderEntriesId"
const spamMailId = offsetId(twoDaysAfterTimeRangeDays)
const spamMail = createTestEntity(MailTypeRef, {
_id: [mailBagMailListId, spamMailId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: spamDetailsId,
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const trashMailId = offsetId(threeDaysAfterTimeRangeDays)
const trashMail = createTestEntity(MailTypeRef, {
_id: [mailBagMailListId, trashMailId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: trashDetailsId,
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const trashSubfolderMailId = offsetId(fourDaysBeforeTimeRangeDays)
const trashSubfolderMail = createTestEntity(MailTypeRef, {
_id: [mailBagMailListId, trashSubfolderMailId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: trashSubfolderDetailsId,
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const spamMailSetEntryElementId = offsetMailSetEntryId(twoDaysAfterTimeRangeDays, spamMailId)
const trashMailSetEntryElementId = offsetMailSetEntryId(threeDaysAfterTimeRangeDays, trashMailId)
const trashSubfolderMailSetEntryElementId = offsetMailSetEntryId(fourDaysBeforeTimeRangeDays, trashSubfolderMailId)
const spamMailSetEntryId: IdTuple = [spamFolderEntriesId, spamMailSetEntryElementId]
const trashMailSetEntryId: IdTuple = [trashFolderEntriesId, trashMailSetEntryElementId]
const trashSubfolderMailSetEntryId: IdTuple = [trashSubfolderEntriesId, trashSubfolderMailSetEntryElementId]
await storage.put(
createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", trashSubfolderId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
parentFolder: ["mailFolderList", trashFolderId],
entries: trashSubfolderEntriesId,
folderType: MailSetKind.CUSTOM,
}),
)
await storage.put(
createTestEntity(MailSetEntryTypeRef, {
_id: spamMailSetEntryId,
mail: spamMail._id,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
}),
)
await storage.put(
createTestEntity(MailSetEntryTypeRef, {
_id: trashMailSetEntryId,
mail: trashMail._id,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
}),
)
await storage.put(
createTestEntity(MailSetEntryTypeRef, {
_id: trashSubfolderMailSetEntryId,
mail: trashSubfolderMail._id,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
}),
)
await storage.put(spamMail)
await storage.put(trashMail)
await storage.put(trashSubfolderMail)
await storage.put(
createTestEntity(MailDetailsBlobTypeRef, {
_id: spamDetailsId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
}),
)
await storage.put(
createTestEntity(MailDetailsBlobTypeRef, {
_id: trashDetailsId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
}),
)
await storage.put(
createTestEntity(MailDetailsBlobTypeRef, {
_id: trashSubfolderDetailsId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
}),
)
// Here we clear the excluded data
await storage.clearExcludedData(timeRangeDays, userId)
// Ensure everything except for the folders themselves is deleted
// Currently, this test is expecting mails to not be deleted as MailOfflineCleaner needs to be reworked
// to support deletion of mails from mailSetEntries. This is currently not possible as we cannot
// partially delete ranges from the ranges db.
// TODO MailSet cleanup
const mailIds = [spamMailId, trashMailId]
const mailDetailsBlobIds = [spamDetailsId, trashDetailsId].map(elementIdPart)
o(await getAllIdsForType(MailTypeRef)).deepEquals(mailIds)
o(await getAllIdsForType(MailSetEntryTypeRef)).deepEquals([])
o(await getAllIdsForType(MailDetailsBlobTypeRef)).deepEquals(mailDetailsBlobIds)
o(await getAllIdsForType(MailFolderTypeRef)).deepEquals([spamFolderId, trashFolderId, trashSubfolderId])
const allListEntities = await dbFacade.all("select * from list_entities", [])
o(allListEntities.map((r) => r.elementId.value)).deepEquals([spamFolderId, trashFolderId, trashSubfolderId].concat(mailIds))
})
o("trash and spam are cleared but mails are only if before timeRangeDays", async function () {
const spamDetailsId: IdTuple = ["detailsListId", "spamDetailsId"]
const trashDetailsId: IdTuple = ["detailsListId", "trashDetailsId"]
const twoDaysAfterTimeRangeDays = 2
const threeDaysBeforeTimeRangeDays = -3
const spamMailId = offsetId(twoDaysAfterTimeRangeDays)
const spamMail = createTestEntity(MailTypeRef, {
_id: [mailBagMailListId, spamMailId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: spamDetailsId,
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const trashMailId = offsetId(threeDaysBeforeTimeRangeDays)
const trashMail = createTestEntity(MailTypeRef, {
_id: [mailBagMailListId, trashMailId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: trashDetailsId,
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const spamMailSetEntryElementId = offsetMailSetEntryId(twoDaysAfterTimeRangeDays, spamMailId)
const trashMailSetEntryElementId = offsetMailSetEntryId(threeDaysBeforeTimeRangeDays, trashMailId)
const spamMailSetEntryId: IdTuple = [spamFolderEntriesId, spamMailSetEntryElementId]
const trashMailSetEntryId: IdTuple = [trashFolderEntriesId, trashMailSetEntryElementId]
await storage.put(
createTestEntity(MailSetEntryTypeRef, {
_id: spamMailSetEntryId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: spamMail._id,
}),
)
await storage.put(
createTestEntity(MailSetEntryTypeRef, {
_id: trashMailSetEntryId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: trashMail._id,
}),
)
await storage.init({ userId, databaseKey, timeRangeDays, forceNewDatabase: false })
await storage.put(spamMail)
await storage.put(trashMail)
await storage.put(
createTestEntity(MailDetailsBlobTypeRef, {
_id: spamDetailsId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
}),
)
await storage.put(
createTestEntity(MailDetailsBlobTypeRef, {
_id: trashDetailsId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
}),
)
// Here we clear the excluded data
await storage.clearExcludedData(timeRangeDays, userId)
// Ensure everything except for the folders themselves is deleted
// Currently, this test is expecting mails to not be deleted as MailOfflineCleaner needs to be reworked
// to support deletion of mails from mailSetEntries. This is currently not possible as we cannot
// partially delete ranges from the ranges db.
// TODO MailSet cleanup
const allEntities = await dbFacade.all("select * from list_entities", [])
o(allEntities.map((r) => r.elementId.value)).deepEquals([spamFolderId, trashFolderId].concat([spamMailId]))
o(await getAllIdsForType(MailFolderTypeRef)).deepEquals([spamFolderId, trashFolderId])
o(await getAllIdsForType(MailSetEntryTypeRef)).deepEquals([])
o(await getAllIdsForType(MailTypeRef)).deepEquals([spamMailId])
o(await getAllIdsForType(MailDetailsBlobTypeRef)).deepEquals([spamDetailsId].map(elementIdPart))
})
o("normal folder is partially cleared", async function () {
const beforeMailDetailsId: IdTuple = ["detailsListId", "beforeDetailsId"]
const afterMailDetailsId: IdTuple = ["detailsListId", "afterDetailsId"]
const inboxFolderId = "inboxFolderId"
const inboxFolderEntriesId: string = "inboxFolderEntriesId"
const twoDaysAfterTimeRangeDays = 2
const twoDaysBeforeTimeRangeDays = -2
const twoDaysBeforeMailId = offsetId(twoDaysBeforeTimeRangeDays)
const twoDaysBeforeMailSetEntryElementId = offsetMailSetEntryId(twoDaysBeforeTimeRangeDays, twoDaysBeforeMailId)
const twoDaysBeforeMailSetEntryId: IdTuple = [inboxFolderEntriesId, twoDaysBeforeMailSetEntryElementId]
const twoDaysAfterMailId = offsetId(twoDaysAfterTimeRangeDays)
const twoDaysAfterMailSetEntryElementId = offsetMailSetEntryId(twoDaysAfterTimeRangeDays, twoDaysAfterMailId)
const twoDaysAfterMailSetEntryId: IdTuple = [inboxFolderEntriesId, twoDaysAfterMailSetEntryElementId]
const mailBefore = createTestEntity(MailTypeRef, {
_id: [mailBagMailListId, offsetId(twoDaysBeforeTimeRangeDays)],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: beforeMailDetailsId,
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const mailAfter = createTestEntity(MailTypeRef, {
_id: [mailBagMailListId, offsetId(twoDaysAfterTimeRangeDays)],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: afterMailDetailsId,
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const mailSetEntryBefore = createTestEntity(MailSetEntryTypeRef, {
_id: twoDaysBeforeMailSetEntryId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: mailBefore._id,
})
const mailSetEntryAfter = createTestEntity(MailSetEntryTypeRef, {
_id: twoDaysAfterMailSetEntryId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: mailAfter._id,
})
const beforeMailDetails = createTestEntity(MailDetailsBlobTypeRef, {
_id: beforeMailDetailsId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
})
const afterMailDetails = createTestEntity(MailDetailsBlobTypeRef, {
_id: afterMailDetailsId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
})
await storage.init({ userId, databaseKey, timeRangeDays, forceNewDatabase: false })
await storage.put(
createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", inboxFolderId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
folderType: MailSetKind.INBOX,
entries: inboxFolderEntriesId,
}),
)
await storage.put(mailBefore)
await storage.put(mailAfter)
await storage.put(mailSetEntryBefore)
await storage.put(mailSetEntryAfter)
await storage.put(beforeMailDetails)
await storage.put(afterMailDetails)
// Here we clear the excluded data
await storage.clearExcludedData(timeRangeDays, userId)
const mailSetEntryTypeModel = await resolveClientTypeReference(MailSetEntryTypeRef)
o(await getAllIdsForType(MailFolderTypeRef)).deepEquals([inboxFolderId, spamFolderId, trashFolderId])
const allMailSetEntryIds = await getAllIdsForType(MailSetEntryTypeRef)
o(allMailSetEntryIds).deepEquals([ensureBase64Ext(mailSetEntryTypeModel, twoDaysAfterMailSetEntryElementId)])
o(await getAllIdsForType(MailTypeRef)).deepEquals([twoDaysAfterMailId])
o(await getAllIdsForType(MailDetailsBlobTypeRef)).deepEquals([afterMailDetailsId].map(elementIdPart))
})
o("normal folder is completely cleared", async function () {
const oneDayBeforeDetailsId: IdTuple = ["detailsListId", "oneDayBeforeDetailsId"]
const twoDaysBeforeDetailsId: IdTuple = ["detailsListId", "twoDaysBeforeDetailsId"]
const inboxFolderId = "inboxFolderId"
const inboxFolderEntriesId: string = "inboxFolderEntriesId"
const oneDayBeforeTimeRangeDays = -1
const twoDaysBeforeTimeRangeDays = -2
const oneDayBeforeMailId = offsetId(oneDayBeforeTimeRangeDays)
const oneDayBeforeMailSetEntryElementId = offsetMailSetEntryId(oneDayBeforeTimeRangeDays, oneDayBeforeMailId)
const oneDayBeforeMailSetEntryId: IdTuple = [inboxFolderEntriesId, oneDayBeforeMailSetEntryElementId]
const twoDaysBeforeMailId = offsetId(twoDaysBeforeTimeRangeDays)
const twoDaysBeforeMailSetEntryElementId = offsetMailSetEntryId(twoDaysBeforeTimeRangeDays, twoDaysBeforeMailId)
const twoDaysBeforeMailSetEntryId: IdTuple = [inboxFolderEntriesId, twoDaysBeforeMailSetEntryElementId]
const mailOneDayBefore = createTestEntity(MailTypeRef, {
_id: [mailBagMailListId, oneDayBeforeMailId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: oneDayBeforeDetailsId,
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const mailTwoDaysBefore = createTestEntity(MailTypeRef, {
_id: [mailBagMailListId, twoDaysBeforeMailId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: twoDaysBeforeDetailsId,
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const mailSetEntryTwoDaysBefore = createTestEntity(MailSetEntryTypeRef, {
_id: twoDaysBeforeMailSetEntryId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: mailTwoDaysBefore._id,
})
const mailSetEntryOneDayBefore = createTestEntity(MailSetEntryTypeRef, {
_id: oneDayBeforeMailSetEntryId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: mailOneDayBefore._id,
})
const oneDayBeforeMailDetails = createTestEntity(MailDetailsBlobTypeRef, {
_id: oneDayBeforeDetailsId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
})
const twoDaysBeforeMailDetails = createTestEntity(MailDetailsBlobTypeRef, {
_id: twoDaysBeforeDetailsId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
})
await storage.init({ userId, databaseKey, timeRangeDays, forceNewDatabase: false })
await storage.put(
createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", inboxFolderId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
folderType: MailSetKind.INBOX,
entries: inboxFolderEntriesId,
}),
)
await storage.put(mailOneDayBefore)
await storage.put(mailTwoDaysBefore)
await storage.put(mailSetEntryTwoDaysBefore)
await storage.put(mailSetEntryOneDayBefore)
await storage.put(oneDayBeforeMailDetails)
await storage.put(twoDaysBeforeMailDetails)
// Here we clear the excluded data
await storage.clearExcludedData(timeRangeDays, userId)
o(await getAllIdsForType(MailFolderTypeRef)).deepEquals([inboxFolderId, spamFolderId, trashFolderId])
const allMailSetEntryIds = await getAllIdsForType(MailSetEntryTypeRef)
o(allMailSetEntryIds).deepEquals([])
o(await getAllIdsForType(MailTypeRef)).deepEquals([])
o(await getAllIdsForType(MailDetailsBlobTypeRef)).deepEquals([])
})
o("when mail is deleted, attachment is also deleted", async function () {
const fileListId = "fileListId"
const beforeMailDetailsId: IdTuple = ["detailsListId", "beforeDetailsId"]
const afterMailDetailsId: IdTuple = ["detailsListId", "afterDetailsId"]
const inboxFolderId = "inboxFolderId"
const inboxFolderEntriesId: string = "inboxFolderEntriesId"
const twoDaysAfterTimeRangeDays = 2
const twoDaysBeforeTimeRangeDays = -2
const twoDaysBeforeMailId = offsetId(twoDaysBeforeTimeRangeDays)
const twoDaysBeforeMailSetEntryElementId = offsetMailSetEntryId(twoDaysBeforeTimeRangeDays, twoDaysBeforeMailId)
const twoDaysBeforeMailSetEntryId: IdTuple = [inboxFolderEntriesId, twoDaysBeforeMailSetEntryElementId]
const twoDaysAfterMailId = offsetId(twoDaysAfterTimeRangeDays)
const twoDaysAfterMailSetEntryElementId = offsetMailSetEntryId(twoDaysAfterTimeRangeDays, twoDaysAfterMailId)
const twoDaysAfterMailSetEntryId: IdTuple = [inboxFolderEntriesId, twoDaysAfterMailSetEntryElementId]
const fileBefore = createTestEntity(FileTypeRef, {
_id: [fileListId, "fileBefore"],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
})
const fileAfter = createTestEntity(FileTypeRef, {
_id: [fileListId, "fileAfter"],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
})
const mailBefore = createTestEntity(MailTypeRef, {
_id: [mailBagMailListId, offsetId(twoDaysBeforeTimeRangeDays)],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: beforeMailDetailsId,
attachments: [fileBefore._id],
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const mailAfter = createTestEntity(MailTypeRef, {
_id: [mailBagMailListId, offsetId(twoDaysAfterTimeRangeDays)],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mailDetails: afterMailDetailsId,
attachments: [fileAfter._id],
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
})
const mailSetEntryBefore = createTestEntity(MailSetEntryTypeRef, {
_id: twoDaysBeforeMailSetEntryId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: mailBefore._id,
})
const mailSetEntryAfter = createTestEntity(MailSetEntryTypeRef, {
_id: twoDaysAfterMailSetEntryId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: mailAfter._id,
})
const beforeMailDetails = createTestEntity(MailDetailsBlobTypeRef, {
_id: beforeMailDetailsId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
})
const afterMailDetails = createTestEntity(MailDetailsBlobTypeRef, {
_id: afterMailDetailsId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
recipients: createTestEntity(RecipientsTypeRef, {}),
body: createTestEntity(BodyTypeRef, {}),
}),
})
await storage.init({ userId, databaseKey, timeRangeDays, forceNewDatabase: false })
await storage.put(
createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", inboxFolderId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
folderType: MailSetKind.INBOX,
entries: "entriesListId",
}),
)
await storage.put(mailSetEntryBefore)
await storage.put(mailSetEntryAfter)
await storage.put(mailBefore)
await storage.put(mailAfter)
await storage.put(fileBefore)
await storage.put(fileAfter)
await storage.put(beforeMailDetails)
await storage.put(afterMailDetails)
// Here we clear the excluded data
await storage.clearExcludedData(timeRangeDays, userId)
o(await getAllIdsForType(MailTypeRef)).deepEquals([getElementId(mailAfter)])
o(await getAllIdsForType(FileTypeRef)).deepEquals([getElementId(fileAfter)])
})
})
})
o.spec("Integration test", function () {
const mailBagMailListId = "mailBagMailListId"
function createMailList(
numMails: number,
idGenerator: IdGenerator,
mailSetEntryIdGenerator: MailSetEntryIdGenerator,
getSubject: (i: number) => string,
getBody: (i: number) => string,
folder: MailFolder,
): { mailSetEntries: Array<MailSetEntry>; mails: Array<Mail>; mailDetailsBlobs: Array<MailDetailsBlob> } {
const mailSetEntries: Array<MailSetEntry> = []
const mails: Array<Mail> = []
const mailDetailsBlobs: Array<MailDetailsBlob> = []
for (let i = 0; i < numMails; ++i) {
const mailId = idGenerator.getNext()
const mailDetailsId = idGenerator.getNext()
const mailSetEntryElementId = mailSetEntryIdGenerator.getNext(mailId)
const mailSetEntryId: IdTuple = [folder.entries, mailSetEntryElementId]
mailSetEntries.push(
createTestEntity(MailSetEntryTypeRef, {
_id: mailSetEntryId,
_ownerGroup: "ownerGroup",
_permissions: "permissions",
mail: [mailBagMailListId, mailId],
}),
)
2022-12-27 15:37:40 +01:00
mails.push(
createTestEntity(MailTypeRef, {
_id: [mailBagMailListId, mailId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
2022-12-27 15:37:40 +01:00
subject: getSubject(i),
sets: [folder._id],
mailDetails: ["detailsListId", mailDetailsId],
sender: createTestEntity(MailAddressTypeRef, {
name: "some name",
address: "address@tuta.com",
}),
conversationEntry: ["listId", "listElementId"],
2022-12-27 15:37:40 +01:00
}),
)
mailDetailsBlobs.push(
createTestEntity(MailDetailsBlobTypeRef, {
_id: ["detailsListId", mailDetailsId],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
details: createTestEntity(MailDetailsTypeRef, {
body: createTestEntity(BodyTypeRef, { text: getBody(i) }),
recipients: createTestEntity(RecipientsTypeRef, {}),
}),
2022-12-27 15:37:40 +01:00
}),
)
}
return { mailSetEntries, mails, mailDetailsBlobs }
}
o("cleanup works as expected", async function () {
// Time range is five days
const oldIds = new IdGenerator(offsetId(-5))
const newIds = new IdGenerator(offsetId(5))
const oldMailSetEntryIds = new MailSetEntryIdGenerator(offsetMailSetEntryId(-5, GENERATED_MIN_ID))
const newMailSetEntryNewIds = new MailSetEntryIdGenerator(offsetMailSetEntryId(5, GENERATED_MIN_ID))
const userMailbox = createTestEntity(MailBoxTypeRef, {
_id: "mailboxId",
_ownerGroup: "ownerGroup",
_permissions: "permissions",
currentMailBag: createTestEntity(MailBagTypeRef, { mails: mailBagMailListId }),
folders: createMailFolderRef({ folders: "mailFolderList" }),
sentAttachments: "sentAttachments",
receivedAttachments: "receivedAttachments",
importedAttachments: "importedAttachments",
mailImportStates: "mailImportStates",
})
const inboxFolder = createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", oldIds.getNext()],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
folderType: MailSetKind.INBOX,
entries: "inboxEntriesListId",
})
const {
mailSetEntries: oldInboxMailSetEntries,
mails: oldInboxMails,
mailDetailsBlobs: oldInboxMailDetailsBlobs,
} = createMailList(
2022-12-27 15:37:40 +01:00
3,
oldIds,
oldMailSetEntryIds,
2022-12-27 15:37:40 +01:00
(i) => `old subject ${i}`,
(i) => `old body ${i}`,
inboxFolder,
2022-12-27 15:37:40 +01:00
)
const {
mailSetEntries: newInboxMailSetEntries,
mails: newInboxMails,
mailDetailsBlobs: newInboxMailDetailsBlobs,
} = createMailList(
2022-12-27 15:37:40 +01:00
3,
newIds,
newMailSetEntryNewIds,
2022-12-27 15:37:40 +01:00
(i) => `new subject ${i}`,
(i) => `new body ${i}`,
inboxFolder,
2022-12-27 15:37:40 +01:00
)
const trashFolder = createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", oldIds.getNext()],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
folderType: MailSetKind.TRASH,
entries: "trashEntriesListId",
})
const {
mailSetEntries: trashMailSetEntries,
mails: trashMails,
mailDetailsBlobs: trashMailDetailsBlobs,
} = createMailList(
2022-12-27 15:37:40 +01:00
3,
newIds,
newMailSetEntryNewIds,
2022-12-27 15:37:40 +01:00
(i) => `trash subject ${i}`,
(i) => `trash body ${i}`,
trashFolder,
2022-12-27 15:37:40 +01:00
)
const spamFolder = createTestEntity(MailFolderTypeRef, {
_id: ["mailFolderList", oldIds.getNext()],
_ownerGroup: "ownerGroup",
_permissions: "permissions",
folderType: MailSetKind.SPAM,
entries: "spamEntriesListId",
})
const everyEntity = [
userMailbox,
2022-12-27 15:37:40 +01:00
inboxFolder,
trashFolder,
spamFolder,
...oldInboxMailSetEntries,
2022-12-27 15:37:40 +01:00
...oldInboxMails,
...oldInboxMailDetailsBlobs,
...newInboxMailSetEntries,
2022-12-27 15:37:40 +01:00
...newInboxMails,
...newInboxMailDetailsBlobs,
...trashMailSetEntries,
2022-12-27 15:37:40 +01:00
...trashMails,
...trashMailDetailsBlobs,
]
2022-12-27 15:37:40 +01:00
await storage.init({ userId, databaseKey: offlineDatabaseTestKey, timeRangeDays, forceNewDatabase: false })
for (let entity of everyEntity) {
await storage.put(entity)
}
await storage.setNewRangeForList(
MailSetEntryTypeRef,
inboxFolder.entries,
elementIdPart(getFirstOrThrow(oldInboxMailSetEntries)._id),
elementIdPart(lastThrow(newInboxMailSetEntries)._id),
)
await storage.setNewRangeForList(
MailSetEntryTypeRef,
trashFolder.entries,
elementIdPart(getFirstOrThrow(trashMailSetEntries)._id),
elementIdPart(lastThrow(trashMailSetEntries)._id),
)
// Here we clear the excluded data
await storage.clearExcludedData(timeRangeDays, userId)
2022-12-27 15:37:40 +01:00
const assertContents = async ({ _id, _type }, expected, msg) => {
const { listId, elementId } = expandId(_id)
let valueFromDb = await storage.get(_type, listId, elementId)
if (valueFromDb && _type === MailTypeRef) {
cleanFieldsFromMail(valueFromDb as Mail)
} else if (valueFromDb && _type === MailDetailsBlobTypeRef) {
clearFieldsFromMailDetailsBlob(valueFromDb as MailDetailsBlob)
} else if (valueFromDb && _type === MailFolderTypeRef) {
delete valueFromDb._finalIvs
}
return o(valueFromDb).deepEquals(expected)(msg)
}
2022-12-27 15:37:40 +01:00
await promiseMap(oldInboxMails, (mail) => assertContents(mail, null, `old mail ${mail._id} was deleted`))
await promiseMap(oldInboxMailDetailsBlobs, (body) => assertContents(body, null, `old mailBody ${body._id} was deleted`))
2022-12-27 15:37:40 +01:00
await promiseMap(newInboxMails, (mail) => assertContents(mail, mail, `new mail ${mail._id} was not deleted`))
await promiseMap(newInboxMailDetailsBlobs, (body) => assertContents(body, body, `new mailBody ${body._id} was not deleted`))
// TODO MailSet cleanup
// All of trash should be cleared, even though the ids are old after Legacy MailFolder cleanup is done
await promiseMap(trashMails, (mail) => assertContents(mail, mail, `trash mail ${mail._id} was deleted`))
await promiseMap(trashMailDetailsBlobs, (body) => assertContents(body, body, `trash mailBody ${body._id} was deleted`))
await assertContents(inboxFolder, inboxFolder, `inbox folder was not deleted`)
await assertContents(trashFolder, trashFolder, `trash folder was not deleted`)
o(await storage.getRangeForList(MailSetEntryTypeRef, inboxFolder.entries)).deepEquals({
// base64Ext encoding is not needed here, as storage.getRangeForList is returning custom elementIds in base64Url already
lower: cuttoffMailSetEntryId,
upper: elementIdPart(lastThrow(newInboxMailSetEntries)._id),
})("lower range for inbox was set to cutoff")
o(await storage.getRangeForList(MailSetEntryTypeRef, trashFolder.entries)).equals(null)("range for trash was deleted")
})
})
2022-12-27 15:37:40 +01:00
})