2023-10-12 17:54:38 +02:00
|
|
|
import o from "@tutao/otest"
|
2024-07-01 17:56:41 +02:00
|
|
|
import { createDataFile } from "../../../../src/common/api/common/DataFile.js"
|
|
|
|
import { DesktopFileFacade } from "../../../../src/common/desktop/files/DesktopFileFacade.js"
|
|
|
|
import { ApplicationWindow } from "../../../../src/common/desktop/ApplicationWindow.js"
|
2024-11-28 18:59:59 +01:00
|
|
|
import { func, matchers, object, verify, when } from "testdouble"
|
2025-03-10 16:19:11 +01:00
|
|
|
import { ElectronExports, FsExports, PathExports } from "../../../../src/common/desktop/ElectronExportTypes.js"
|
2024-11-28 18:59:59 +01:00
|
|
|
import { NotFoundError, PreconditionFailedError, TooManyRequestsError } from "../../../../src/common/api/common/error/RestError.js"
|
2023-10-12 17:54:38 +02:00
|
|
|
import type fs from "node:fs"
|
|
|
|
import { assertThrows } from "@tutao/tutanota-test-utils"
|
|
|
|
import { stringToUtf8Uint8Array } from "@tutao/tutanota-utils"
|
2024-07-01 17:56:41 +02:00
|
|
|
import { DesktopConfig } from "../../../../src/common/desktop/config/DesktopConfig.js"
|
|
|
|
import { DesktopUtils } from "../../../../src/common/desktop/DesktopUtils.js"
|
|
|
|
import { DateProvider } from "../../../../src/common/api/common/DateProvider.js"
|
|
|
|
import { TempFs } from "../../../../src/common/desktop/files/TempFs.js"
|
|
|
|
import { BuildConfigKey, DesktopConfigKey } from "../../../../src/common/desktop/config/ConfigKeys.js"
|
2024-11-28 18:59:59 +01:00
|
|
|
import { HttpMethod } from "../../../../src/common/api/common/EntityFunctions"
|
|
|
|
import { FetchImpl, FetchResult } from "../../../../src/common/desktop/net/NetAgent"
|
2025-09-25 18:05:10 +02:00
|
|
|
import { CommandExecutor } from "../../../../src/common/desktop/CommandExecutor"
|
2023-10-12 17:54:38 +02:00
|
|
|
|
|
|
|
const DEFAULT_DOWNLOAD_PATH = "/a/download/path/"
|
|
|
|
|
|
|
|
o.spec("DesktopFileFacade", function () {
|
|
|
|
let win: ApplicationWindow
|
|
|
|
let conf: DesktopConfig
|
|
|
|
let du: DesktopUtils
|
|
|
|
let dp: DateProvider
|
2024-11-28 18:59:59 +01:00
|
|
|
let fetch: FetchImpl
|
2023-10-12 17:54:38 +02:00
|
|
|
let electron: ElectronExports
|
|
|
|
let fs: FsExports
|
|
|
|
let tfs: TempFs
|
|
|
|
let ff: DesktopFileFacade
|
2025-03-10 16:19:11 +01:00
|
|
|
let path: PathExports
|
2025-09-25 18:05:10 +02:00
|
|
|
let executor: CommandExecutor
|
|
|
|
let process: Writeable<Partial<NodeJS.Process>>
|
2023-10-12 17:54:38 +02:00
|
|
|
|
|
|
|
o.beforeEach(function () {
|
|
|
|
win = object()
|
2024-11-28 18:59:59 +01:00
|
|
|
fetch = func() as FetchImpl
|
2023-10-12 17:54:38 +02:00
|
|
|
fs = object()
|
|
|
|
tfs = object()
|
2025-03-10 16:19:11 +01:00
|
|
|
path = object()
|
2023-10-12 17:54:38 +02:00
|
|
|
fs.promises = object()
|
2025-09-25 18:05:10 +02:00
|
|
|
process = {
|
|
|
|
env: {},
|
|
|
|
}
|
2024-11-15 15:01:02 +01:00
|
|
|
when(fs.promises.stat(matchers.anything())).thenResolve({ size: 42 })
|
2023-10-12 17:54:38 +02:00
|
|
|
electron = object()
|
|
|
|
// @ts-ignore read-only prop
|
2024-11-28 18:59:59 +01:00
|
|
|
electron["shell"] = object()
|
2023-10-12 17:54:38 +02:00
|
|
|
// @ts-ignore read-only prop
|
2024-11-28 18:59:59 +01:00
|
|
|
electron["dialog"] = object()
|
2025-09-25 18:05:10 +02:00
|
|
|
process.platform = "linux"
|
2023-10-12 17:54:38 +02:00
|
|
|
|
|
|
|
conf = object()
|
|
|
|
du = object()
|
|
|
|
dp = object()
|
2025-09-25 18:05:10 +02:00
|
|
|
executor = object()
|
2023-10-12 17:54:38 +02:00
|
|
|
|
2025-09-25 18:05:10 +02:00
|
|
|
ff = new DesktopFileFacade(win, conf, dp, fetch, electron, tfs, fs, path, executor, process as NodeJS.Process)
|
2023-10-12 17:54:38 +02:00
|
|
|
})
|
|
|
|
o.spec("saveDataFile", function () {
|
|
|
|
o("when there's no existing file it will be simply written", async function () {
|
|
|
|
const dataFile = createDataFile("blob", "application/octet-stream", new Uint8Array([1]))
|
|
|
|
when(fs.promises.readdir(matchers.anything())).thenResolve(["somethingelse"])
|
|
|
|
when(fs.promises.mkdir("/tutanota/tmp/path/download", { recursive: true })).thenResolve(undefined)
|
|
|
|
when(fs.promises.writeFile("/tutanota/tmp/path/download/blob", dataFile.data)).thenResolve()
|
2025-03-10 16:19:11 +01:00
|
|
|
await ff.writeTempDataFile(dataFile)
|
2023-10-12 17:54:38 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
o("with default download path but file exists -> nonclobbering name is chosen", async function () {
|
|
|
|
const e = new Error() as any
|
|
|
|
e.code = "EEXISTS"
|
|
|
|
const dataFile = createDataFile("blob", "application/octet-stream", new Uint8Array([1]))
|
|
|
|
when(fs.promises.writeFile("/tutanota/tmp/path/download/blob", matchers.anything())).thenReject(e)
|
|
|
|
when(fs.promises.readdir(matchers.anything())).thenResolve(["blob"])
|
|
|
|
when(fs.promises.mkdir("/tutanota/tmp/path/download", { recursive: true })).thenResolve(undefined)
|
|
|
|
when(fs.promises.writeFile("/tutanota/tmp/path/download/blob-1", dataFile.data)).thenResolve()
|
2025-03-10 16:19:11 +01:00
|
|
|
await ff.writeTempDataFile(dataFile)
|
2023-10-12 17:54:38 +02:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2023-12-18 11:36:38 +01:00
|
|
|
o.spec("download", function () {
|
2023-10-12 17:54:38 +02:00
|
|
|
o("no error", async function () {
|
|
|
|
const headers = { v: "foo", accessToken: "bar" }
|
|
|
|
const expectedFilePath = "/tutanota/tmp/path/encrypted/nativelyDownloadedFile"
|
2024-11-28 18:59:59 +01:00
|
|
|
const response: FetchResult = mockResponse(200, { responseBody: new Uint8Array() })
|
2023-10-12 17:54:38 +02:00
|
|
|
const ws: fs.WriteStream = mockWriteStream(response)
|
|
|
|
when(fs.createWriteStream(expectedFilePath, { emitClose: true })).thenReturn(ws)
|
2024-11-28 18:59:59 +01:00
|
|
|
when(
|
|
|
|
fetch(urlMatches(new URL("some://url/file")), {
|
2023-10-12 17:54:38 +02:00
|
|
|
method: "GET",
|
|
|
|
headers,
|
|
|
|
}),
|
|
|
|
).thenResolve(response)
|
|
|
|
// @ts-ignore callback omit
|
|
|
|
when(ws.on("finish")).thenCallback(undefined, undefined)
|
|
|
|
when(tfs.ensureEncryptedDir()).thenResolve("/tutanota/tmp/path/encrypted")
|
|
|
|
|
|
|
|
const downloadResult = await ff.download("some://url/file", "nativelyDownloadedFile", headers)
|
|
|
|
o(downloadResult.statusCode).equals(200)
|
|
|
|
o(downloadResult.encryptedFileUri).equals(expectedFilePath)
|
|
|
|
})
|
|
|
|
|
|
|
|
o("404 error gets returned", async function () {
|
|
|
|
const headers = {
|
|
|
|
v: "foo",
|
|
|
|
accessToken: "bar",
|
|
|
|
}
|
|
|
|
|
|
|
|
const errorId = "123"
|
2024-11-28 18:59:59 +01:00
|
|
|
const response: FetchResult = mockResponse(NotFoundError.CODE, {
|
|
|
|
responseHeaders: {
|
|
|
|
"error-id": errorId,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)
|
2023-10-12 17:54:38 +02:00
|
|
|
const result = await ff.download("some://url/file", "nativelyDownloadedFile", headers)
|
|
|
|
|
|
|
|
o(result).deepEquals({
|
|
|
|
statusCode: 404,
|
|
|
|
errorId,
|
|
|
|
precondition: null,
|
|
|
|
suspensionTime: null,
|
|
|
|
encryptedFileUri: null,
|
|
|
|
})
|
|
|
|
verify(fs.createWriteStream(matchers.anything(), matchers.anything()), { times: 0 })
|
|
|
|
})
|
|
|
|
|
|
|
|
o("retry-after", async function () {
|
|
|
|
const retryAfter = "20"
|
2024-11-28 18:59:59 +01:00
|
|
|
const errorId = "123"
|
|
|
|
|
|
|
|
const response: FetchResult = mockResponse(TooManyRequestsError.CODE, {
|
|
|
|
responseHeaders: {
|
|
|
|
"error-id": errorId,
|
|
|
|
"retry-after": retryAfter,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)
|
2023-10-12 17:54:38 +02:00
|
|
|
|
|
|
|
const headers = { v: "foo", accessToken: "bar" }
|
|
|
|
const result = await ff.download("some://url/file", "nativelyDownloadedFile", headers)
|
|
|
|
|
|
|
|
o(result).deepEquals({
|
|
|
|
statusCode: TooManyRequestsError.CODE,
|
|
|
|
errorId,
|
|
|
|
precondition: null,
|
|
|
|
suspensionTime: retryAfter,
|
|
|
|
encryptedFileUri: null,
|
|
|
|
})
|
|
|
|
verify(fs.createWriteStream(matchers.anything(), matchers.anything()), { times: 0 })
|
|
|
|
})
|
|
|
|
|
|
|
|
o("suspension", async function () {
|
|
|
|
const headers = { v: "foo", accessToken: "bar" }
|
|
|
|
const errorId = "123"
|
|
|
|
const retryAfter = "20"
|
2024-11-28 18:59:59 +01:00
|
|
|
const response: FetchResult = mockResponse(TooManyRequestsError.CODE, {
|
|
|
|
responseHeaders: {
|
|
|
|
"error-id": errorId,
|
|
|
|
"suspension-time": retryAfter,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)
|
2023-10-12 17:54:38 +02:00
|
|
|
|
|
|
|
const result = await ff.download("some://url/file", "nativelyDownloadedFile", headers)
|
|
|
|
|
|
|
|
o(result).deepEquals({
|
|
|
|
statusCode: TooManyRequestsError.CODE,
|
|
|
|
errorId,
|
|
|
|
precondition: null,
|
|
|
|
suspensionTime: retryAfter,
|
|
|
|
encryptedFileUri: null,
|
|
|
|
})
|
|
|
|
verify(fs.createWriteStream(matchers.anything(), matchers.anything()), { times: 0 })
|
|
|
|
})
|
|
|
|
|
|
|
|
o("precondition", async function () {
|
|
|
|
const headers = { v: "foo", accessToken: "bar" }
|
|
|
|
const errorId = "123"
|
|
|
|
const precondition = "a.2"
|
2024-11-28 18:59:59 +01:00
|
|
|
const response: FetchResult = mockResponse(PreconditionFailedError.CODE, {
|
|
|
|
responseHeaders: {
|
|
|
|
"error-id": errorId,
|
|
|
|
precondition: precondition,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)
|
2023-10-12 17:54:38 +02:00
|
|
|
|
|
|
|
const result = await ff.download("some://url/file", "nativelyDownloadedFile", headers)
|
|
|
|
|
|
|
|
o(result).deepEquals({
|
|
|
|
statusCode: PreconditionFailedError.CODE,
|
|
|
|
errorId,
|
|
|
|
precondition,
|
|
|
|
suspensionTime: null,
|
|
|
|
encryptedFileUri: null,
|
|
|
|
})
|
|
|
|
verify(fs.createWriteStream(matchers.anything(), matchers.anything()), { times: 0 })
|
|
|
|
})
|
|
|
|
|
2024-11-28 18:59:59 +01:00
|
|
|
o("IO error during download leads to cleanup and error is thrown", async function () {
|
2023-10-12 17:54:38 +02:00
|
|
|
const headers = { v: "foo", accessToken: "bar" }
|
2024-11-28 18:59:59 +01:00
|
|
|
const response: FetchResult = mockResponse(200, { responseBody: new Uint8Array() })
|
|
|
|
when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)
|
2023-10-12 17:54:38 +02:00
|
|
|
const error = new Error("Test! I/O error")
|
|
|
|
const ws = mockWriteStream()
|
2024-11-28 18:59:59 +01:00
|
|
|
when(ws.on("finish", matchers.anything())).thenThrow(error)
|
2023-10-12 17:54:38 +02:00
|
|
|
when(fs.createWriteStream(matchers.anything(), matchers.anything())).thenReturn(ws)
|
|
|
|
when(tfs.ensureEncryptedDir()).thenResolve("/tutanota/tmp/path/encrypted")
|
|
|
|
|
|
|
|
const e = await assertThrows(Error, () => ff.download("some://url/file", "nativelyDownloadedFile", headers))
|
|
|
|
o(e).equals(error)
|
|
|
|
verify(fs.promises.unlink("/tutanota/tmp/path/encrypted/nativelyDownloadedFile"), { times: 1 })
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2023-12-18 11:36:38 +01:00
|
|
|
o.spec("upload", function () {
|
2023-10-12 17:54:38 +02:00
|
|
|
const fileToUploadPath = "/tutnaota/tmp/path/encrypted/toUpload.txt"
|
|
|
|
const targetUrl = "https://test.tutanota.com/rest/for/a/bit"
|
|
|
|
|
|
|
|
o("when there's no error it uploads correct data and returns the right result", async function () {
|
|
|
|
const body = stringToUtf8Uint8Array("BODY")
|
|
|
|
const response = mockResponse(200, { responseBody: body })
|
|
|
|
const headers = {
|
|
|
|
blobAccessToken: "1236",
|
|
|
|
}
|
|
|
|
const fileStreamMock = mockReadStream()
|
|
|
|
when(fs.createReadStream(fileToUploadPath)).thenReturn(fileStreamMock)
|
2024-11-15 15:01:02 +01:00
|
|
|
when(
|
2024-11-28 18:59:59 +01:00
|
|
|
fetch(urlMatches(new URL(targetUrl)), {
|
|
|
|
method: HttpMethod.POST,
|
|
|
|
headers,
|
|
|
|
body: fileStreamMock,
|
|
|
|
}),
|
2024-11-15 15:01:02 +01:00
|
|
|
).thenResolve(response)
|
2024-11-28 18:59:59 +01:00
|
|
|
const uploadResult = await ff.upload(fileToUploadPath, targetUrl, HttpMethod.POST, headers)
|
2023-10-12 17:54:38 +02:00
|
|
|
|
|
|
|
o(uploadResult.statusCode).equals(200)
|
|
|
|
o(uploadResult.errorId).equals(null)
|
|
|
|
o(uploadResult.precondition).equals(null)
|
|
|
|
o(uploadResult.suspensionTime).equals(null)
|
|
|
|
o(Array.from(uploadResult.responseBody)).deepEquals(Array.from(body))
|
|
|
|
})
|
|
|
|
|
|
|
|
o("when 404 is returned it returns correct result", async function () {
|
|
|
|
const errorId = "123"
|
|
|
|
const response = mockResponse(404, { responseHeaders: { "error-id": errorId } })
|
2024-11-28 18:59:59 +01:00
|
|
|
when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)
|
|
|
|
const uploadResult = await ff.upload(fileToUploadPath, targetUrl, HttpMethod.POST, {})
|
2023-10-12 17:54:38 +02:00
|
|
|
o(uploadResult.statusCode).equals(404)
|
|
|
|
o(uploadResult.errorId).equals(errorId)
|
|
|
|
o(uploadResult.precondition).equals(null)
|
|
|
|
o(uploadResult.suspensionTime).equals(null)
|
|
|
|
o(Array.from(uploadResult.responseBody)).deepEquals([])
|
|
|
|
})
|
|
|
|
|
|
|
|
o("when retry-after is returned, it is propagated", async function () {
|
|
|
|
const retryAFter = "20"
|
|
|
|
const errorId = "123"
|
|
|
|
const response = mockResponse(TooManyRequestsError.CODE, {
|
|
|
|
responseHeaders: {
|
|
|
|
"error-id": errorId,
|
|
|
|
"retry-after": retryAFter,
|
|
|
|
},
|
|
|
|
})
|
2024-11-28 18:59:59 +01:00
|
|
|
when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)
|
2023-10-12 17:54:38 +02:00
|
|
|
|
2024-11-28 18:59:59 +01:00
|
|
|
const uploadResult = await ff.upload(fileToUploadPath, targetUrl, HttpMethod.POST, {})
|
2023-10-12 17:54:38 +02:00
|
|
|
|
|
|
|
o(uploadResult.statusCode).equals(TooManyRequestsError.CODE)
|
|
|
|
o(uploadResult.errorId).equals(errorId)
|
|
|
|
o(uploadResult.precondition).equals(null)
|
|
|
|
o(uploadResult.suspensionTime).equals(retryAFter)
|
|
|
|
o(Array.from(uploadResult.responseBody)).deepEquals([])
|
|
|
|
})
|
|
|
|
|
|
|
|
o("when suspension-time is returned, it is propagated", async function () {
|
|
|
|
const retryAFter = "20"
|
|
|
|
const errorId = "123"
|
|
|
|
const response = mockResponse(TooManyRequestsError.CODE, {
|
|
|
|
responseHeaders: {
|
|
|
|
"error-id": errorId,
|
|
|
|
"suspension-time": retryAFter,
|
|
|
|
},
|
|
|
|
})
|
2024-11-28 18:59:59 +01:00
|
|
|
when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)
|
|
|
|
const uploadResult = await ff.upload(fileToUploadPath, targetUrl, HttpMethod.POST, {})
|
2023-10-12 17:54:38 +02:00
|
|
|
|
|
|
|
o(uploadResult.statusCode).equals(TooManyRequestsError.CODE)
|
|
|
|
o(uploadResult.errorId).equals(errorId)
|
|
|
|
o(uploadResult.precondition).equals(null)
|
|
|
|
o(uploadResult.suspensionTime).equals(retryAFter)
|
|
|
|
o(Array.from(uploadResult.responseBody)).deepEquals([])
|
|
|
|
})
|
|
|
|
|
|
|
|
o("when precondition-time is returned, it is propagated", async function () {
|
|
|
|
const precondition = "a.2"
|
|
|
|
const errorId = "123"
|
|
|
|
const response = mockResponse(PreconditionFailedError.CODE, {
|
|
|
|
responseHeaders: {
|
|
|
|
"error-id": errorId,
|
|
|
|
precondition: precondition,
|
|
|
|
},
|
|
|
|
})
|
2024-11-28 18:59:59 +01:00
|
|
|
when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)
|
2023-10-12 17:54:38 +02:00
|
|
|
|
2024-11-28 18:59:59 +01:00
|
|
|
const uploadResult = await ff.upload(fileToUploadPath, targetUrl, HttpMethod.POST, {})
|
2023-10-12 17:54:38 +02:00
|
|
|
|
|
|
|
o(uploadResult.statusCode).equals(PreconditionFailedError.CODE)
|
|
|
|
o(uploadResult.errorId).equals(errorId)
|
|
|
|
o(uploadResult.precondition).equals(precondition)
|
|
|
|
o(uploadResult.suspensionTime).equals(null)
|
|
|
|
o(Array.from(uploadResult.responseBody)).deepEquals([])
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
o.spec("open", function () {
|
2025-09-25 18:05:10 +02:00
|
|
|
o.spec("open on linux", () => {
|
|
|
|
o.test("open on ubuntu", async function () {
|
|
|
|
when(electron.shell.openPath("/some/folder/file")).thenReject(new Error("wrong function"))
|
|
|
|
when(executor.run(matchers.anything())).thenResolve({})
|
|
|
|
process.env!.ORIGINAL_XDG_CURRENT_DESKTOP = "original ;)"
|
|
|
|
process.env!.SOMETHING_ELSE = "something else!"
|
|
|
|
await ff.open("/some/folder/file")
|
|
|
|
|
|
|
|
verify(
|
|
|
|
executor.run({
|
|
|
|
executable: "xdg-open",
|
|
|
|
args: ["/some/folder/file"],
|
|
|
|
env: {
|
|
|
|
XDG_CURRENT_DESKTOP: "original ;)",
|
|
|
|
ORIGINAL_XDG_CURRENT_DESKTOP: "original ;)",
|
|
|
|
SOMETHING_ELSE: "something else!",
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
o.test("open on non ubuntu", async function () {
|
|
|
|
when(electron.shell.openPath("/some/folder/file")).thenReject(new Error("wrong function"))
|
|
|
|
when(executor.run(matchers.anything())).thenResolve({})
|
|
|
|
process.env!.ORIGINAL_XDG_CURRENT_DESKTOP = undefined
|
|
|
|
process.env!.SOMETHING_ELSE = "something else!"
|
|
|
|
await ff.open("/some/folder/file")
|
|
|
|
|
|
|
|
verify(
|
|
|
|
executor.run({
|
|
|
|
executable: "xdg-open",
|
|
|
|
args: ["/some/folder/file"],
|
|
|
|
env: undefined,
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
})
|
2023-10-12 17:54:38 +02:00
|
|
|
})
|
|
|
|
|
2025-09-25 18:05:10 +02:00
|
|
|
o.test("open on windows", async function () {
|
|
|
|
process.platform = "win32"
|
2024-11-15 15:01:02 +01:00
|
|
|
when(electron.dialog.showMessageBox(matchers.anything())).thenReturn(
|
|
|
|
Promise.resolve({
|
|
|
|
response: 1,
|
|
|
|
checkboxChecked: false,
|
|
|
|
}),
|
|
|
|
)
|
2023-10-12 17:54:38 +02:00
|
|
|
await ff.open("exec.exe")
|
|
|
|
verify(electron.shell.openPath(matchers.anything()), { times: 0 })
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
o.spec("join", function () {
|
|
|
|
o("join a single file", async function () {
|
|
|
|
const ws: fs.WriteStream = mockWriteStream()
|
|
|
|
const rs: fs.ReadStream = mockReadStream(ws)
|
|
|
|
when(fs.createWriteStream(matchers.anything(), matchers.anything())).thenReturn(ws)
|
|
|
|
when(fs.createReadStream(matchers.anything())).thenReturn(rs)
|
|
|
|
// @ts-ignore callback omit
|
|
|
|
when(rs.on("end")).thenCallback(undefined, undefined)
|
|
|
|
when(fs.promises.readdir("/tutanota/tmp/path/unencrypted")).thenResolve(["folderContents"])
|
|
|
|
when(tfs.ensureUnencrytpedDir()).thenResolve("/tutanota/tmp/path/unencrypted")
|
|
|
|
const joinedFilePath = await ff.joinFiles("fileName.pdf", ["/file1"])
|
|
|
|
o(joinedFilePath).equals("/tutanota/tmp/path/unencrypted/fileName.pdf")
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
o.spec("splitFile", function () {
|
|
|
|
o("returns one slice for a small file", async function () {
|
|
|
|
// fs mock returns file name as the content
|
|
|
|
const filename = "/tutanota/tmp/path/download/small.txt"
|
|
|
|
const fileContent = stringToUtf8Uint8Array(filename)
|
|
|
|
const filenameHash = "9ca089f82e397e9e860daa312ac25def39f2da0e066f0de94ffc02aa7b3a6250"
|
|
|
|
const expectedChunkPath = `/tutanota/tmp/path/unencrypted/${filenameHash}.0.blob`
|
|
|
|
when(tfs.ensureUnencrytpedDir()).thenResolve("/tutanota/tmp/path/unencrypted")
|
|
|
|
when(fs.promises.writeFile(expectedChunkPath, fileContent)).thenResolve()
|
|
|
|
when(fs.promises.readFile(filename)).thenResolve(Buffer.from(fileContent))
|
|
|
|
const chunks = await ff.splitFile(filename, 1024)
|
|
|
|
o(chunks).deepEquals([expectedChunkPath])("only one chunk")
|
|
|
|
})
|
|
|
|
|
|
|
|
o("returns multiple slices for a bigger file", async function () {
|
|
|
|
// fs mock returns file name as the content
|
|
|
|
const filename = "/tutanota/tmp/path/download/big.txt"
|
|
|
|
// length 37
|
|
|
|
const fileContent = stringToUtf8Uint8Array(filename)
|
|
|
|
const filenameHash = "c24646a4738a92d624cd03134f26c371d8a2950d2b3bbce7921c288de9a56fd3"
|
|
|
|
const expectedChunkPath0 = `/tutanota/tmp/path/unencrypted/${filenameHash}.0.blob`
|
|
|
|
const expectedChunkPath1 = `/tutanota/tmp/path/unencrypted/${filenameHash}.1.blob`
|
|
|
|
|
|
|
|
when(tfs.ensureUnencrytpedDir()).thenResolve("/tutanota/tmp/path/unencrypted")
|
|
|
|
when(fs.promises.writeFile(expectedChunkPath0, fileContent.slice(0, 30))).thenResolve()
|
|
|
|
when(fs.promises.writeFile(expectedChunkPath1, fileContent.slice(30))).thenResolve()
|
|
|
|
when(fs.promises.readFile(filename)).thenResolve(Buffer.from(fileContent))
|
|
|
|
const chunks = await ff.splitFile(filename, 30)
|
|
|
|
o(chunks).deepEquals([expectedChunkPath0, expectedChunkPath1])("both written files are in the returned array")
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
o.spec("showInFileExplorer", function () {
|
|
|
|
o("two downloads, open two filemanagers", async function () {
|
|
|
|
const dir = "/path/to"
|
|
|
|
const p = dir + "/file.txt"
|
|
|
|
await ff.showInFileExplorer(p)
|
2025-09-25 18:05:10 +02:00
|
|
|
verify(electron.shell.showItemInFolder(p), { times: 1 })
|
2023-10-12 17:54:38 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
o("two downloads, open two filemanagers after a pause", async function () {
|
|
|
|
const time = 1629115820468
|
|
|
|
const dir = "/path/to"
|
|
|
|
const p = dir + "/file.txt"
|
|
|
|
await ff.showInFileExplorer(p)
|
|
|
|
when(dp.now()).thenReturn(time)
|
|
|
|
when(conf.getConst(BuildConfigKey.fileManagerTimeout)).thenResolve(2)
|
2025-09-25 18:05:10 +02:00
|
|
|
verify(electron.shell.showItemInFolder(p), { times: 1 })
|
2023-10-12 17:54:38 +02:00
|
|
|
when(dp.now()).thenReturn(time + 10)
|
|
|
|
await ff.showInFileExplorer(p)
|
2025-09-25 18:05:10 +02:00
|
|
|
verify(electron.shell.showItemInFolder(p), { times: 2 })
|
2023-10-12 17:54:38 +02:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
o.spec("putFileIntoDownloadsFolder", function () {
|
|
|
|
o("putFileIntoDownloadsFolder", async function () {
|
|
|
|
const src = "/path/random.pdf"
|
|
|
|
const filename = "fileName.pdf"
|
|
|
|
when(conf.getVar(DesktopConfigKey.defaultDownloadPath)).thenResolve(DEFAULT_DOWNLOAD_PATH)
|
|
|
|
when(fs.promises.readdir(matchers.anything())).thenResolve([])
|
|
|
|
const copiedFileUri = await ff.putFileIntoDownloadsFolder(src, filename)
|
|
|
|
verify(fs.promises.copyFile(src, DEFAULT_DOWNLOAD_PATH + "fileName.pdf"))
|
|
|
|
o(copiedFileUri).equals(DEFAULT_DOWNLOAD_PATH + "fileName.pdf")
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
o.spec("size", function () {
|
|
|
|
o("size", async function () {
|
|
|
|
when(fs.promises.stat(matchers.anything())).thenResolve({ size: 33 })
|
|
|
|
o(await ff.getSize("/file1")).equals(33)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
o.spec("hash", function () {
|
|
|
|
o("hash", async function () {
|
|
|
|
when(fs.promises.readFile("/file1")).thenResolve(new Uint8Array([0, 1, 2, 3]) as Buffer)
|
|
|
|
o(await ff.hashFile("/file1")).equals("BU7ewdAh")
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
function mockReadStream(ws?: fs.WriteStream): fs.ReadStream {
|
|
|
|
const rs: fs.ReadStream = object()
|
|
|
|
if (ws != null) {
|
|
|
|
when(rs.pipe(ws, { end: false })).thenReturn(ws)
|
|
|
|
}
|
|
|
|
|
|
|
|
return rs
|
|
|
|
}
|
2024-03-11 16:25:43 +01:00
|
|
|
|
|
|
|
const urlMatches = matchers.create({
|
|
|
|
name: "urlMatches",
|
|
|
|
matches(matcherArgs: any[], actual: any): boolean {
|
|
|
|
return (actual as URL).toString() === (matcherArgs[0] as URL).toString()
|
|
|
|
},
|
|
|
|
})
|
2024-11-28 18:59:59 +01:00
|
|
|
|
|
|
|
function mockWriteStream(response?: FetchResult): fs.WriteStream {
|
|
|
|
const ws: fs.WriteStream = object()
|
|
|
|
if (response != null) {
|
|
|
|
// when(response.pipe(ws)).thenReturn(ws)
|
|
|
|
}
|
|
|
|
const closeCapturer = matchers.captor()
|
|
|
|
when(ws.on("close", closeCapturer.capture())).thenReturn(ws)
|
|
|
|
when(ws.close()).thenDo(() => closeCapturer.value())
|
|
|
|
return ws
|
|
|
|
}
|
|
|
|
|
|
|
|
function mockResponse(
|
|
|
|
statusCode: number,
|
|
|
|
resOpts: {
|
|
|
|
responseBody?: Uint8Array
|
|
|
|
responseHeaders?: Record<string, string>
|
|
|
|
},
|
|
|
|
): FetchResult {
|
|
|
|
const { responseBody, responseHeaders } = resOpts
|
|
|
|
return new global.Response(responseBody, {
|
|
|
|
status: statusCode,
|
|
|
|
headers: new Headers(responseHeaders),
|
|
|
|
}) as FetchResult
|
|
|
|
}
|