import { describe, it, expect, beforeEach, vi } from "vitest"; import type { getTriliumDataDir as getTriliumDataDirType, getDataDirs as getDataDirsType, getPlatformAppDataDir as getPlatformAppDataDirType } from "../src/services/data_dir"; describe("data_dir.ts unit tests", async () => { let getTriliumDataDir: typeof getTriliumDataDirType; let getPlatformAppDataDir: typeof getPlatformAppDataDirType; let getDataDirs: typeof getDataDirsType; const mockFn = { existsSyncMock: vi.fn(), mkdirSyncMock: vi.fn(), osHomedirMock: vi.fn(), osPlatformMock: vi.fn(), pathJoinMock: vi.fn() }; // using doMock, to avoid hoisting, so that we can use the mockFn object // to collect all mocked Fns vi.doMock("node:fs", () => { return { default: { existsSync: mockFn.existsSyncMock, mkdirSync: mockFn.mkdirSyncMock } }; }); vi.doMock("node:os", () => { return { default: { homedir: mockFn.osHomedirMock, platform: mockFn.osPlatformMock } }; }); vi.doMock("path", () => { return { join: mockFn.pathJoinMock }; }); // import function to test now, after creating the mocks ({ getTriliumDataDir } = await import("../src/services/data_dir.ts")); ({ getPlatformAppDataDir } = await import("../src/services/data_dir.ts")); ({ getDataDirs } = await import("../src/services/data_dir.ts")); // helper to reset call counts const resetAllMocks = () => { Object.values(mockFn).forEach((mockedFn) => { mockedFn.mockReset(); }); }; // helper to set mocked Platform const setMockPlatform = (osPlatform: string, homedir: string, pathJoin: string) => { mockFn.osPlatformMock.mockImplementation(() => osPlatform); mockFn.osHomedirMock.mockImplementation(() => homedir); mockFn.pathJoinMock.mockImplementation(() => pathJoin); }; describe("#getPlatformAppDataDir()", () => { type TestCaseGetPlatformAppDataDir = [description: string, fnValue: Parameters, expectedValueFn: (val: ReturnType) => boolean]; const testCases: TestCaseGetPlatformAppDataDir[] = [ ["w/ unsupported OS it should return 'null'", ["aix", undefined], (val) => val === null], ["w/ win32 and no APPDATA set it should return 'null'", ["win32", undefined], (val) => val === null], ["w/ win32 and set APPDATA it should return set 'APPDATA'", ["win32", "AppData"], (val) => val === "AppData"], ["w/ linux it should return '/.local/share'", ["linux", undefined], (val) => val !== null && val.endsWith("/.local/share")], ["w/ linux and wrongly set APPDATA it should ignore APPDATA and return /.local/share", ["linux", "FakeAppData"], (val) => val !== null && val.endsWith("/.local/share")], ["w/ darwin it should return /Library/Application Support", ["darwin", undefined], (val) => val !== null && val.endsWith("/Library/Application Support")] ]; testCases.forEach((testCase) => { const [testDescription, value, isExpected] = testCase; return it(testDescription, () => { const actual = getPlatformAppDataDir(...value); const result = isExpected(actual); expect(result).toBeTruthy(); }); }); }); describe("#getTriliumDataDir", async () => { beforeEach(() => { // make sure these are not set delete process.env.TRILIUM_DATA_DIR; delete process.env.APPDATA; resetAllMocks(); }); /** * case A – process.env.TRILIUM_DATA_DIR is set * case B – process.env.TRILIUM_DATA_DIR is not set and Trilium folder is existing in platform * case C – process.env.TRILIUM_DATA_DIR is not set and Trilium folder is not existing in platform's home dir * case D – fallback to creating Trilium folder in home dir */ describe("case A", () => { it("when folder exists – it should return the path, without attempting to create the folder", async () => { const mockTriliumDataPath = "/home/mock/trilium-data-ENV-A1"; process.env.TRILIUM_DATA_DIR = mockTriliumDataPath; // set fs.existsSync to true, i.e. the folder does exist mockFn.existsSyncMock.mockImplementation(() => true); const result = getTriliumDataDir("trilium-data"); // createDirIfNotExisting should call existsync 1 time and mkdirSync 0 times -> as it does not need to create the folder // and return value should be TRILIUM_DATA_DIR value from process.env expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(1); expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(0); expect(result).toEqual(process.env.TRILIUM_DATA_DIR); }); it("when folder does not exist – it should attempt to create the folder and return the path", async () => { const mockTriliumDataPath = "/home/mock/trilium-data-ENV-A2"; process.env.TRILIUM_DATA_DIR = mockTriliumDataPath; // set fs.existsSync mock to return false, i.e. the folder does not exist mockFn.existsSyncMock.mockImplementation(() => false); const result = getTriliumDataDir("trilium-data"); // createDirIfNotExisting should call existsync 1 time and mkdirSync 1 times -> as it has to create the folder // and return value should be TRILIUM_DATA_DIR value from process.env expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(1); expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1); expect(result).toEqual(process.env.TRILIUM_DATA_DIR); }); }); describe("case B", () => { it("it should check if folder exists and return it", async () => { const homedir = "/home/mock"; const dataDirName = "trilium-data"; const mockTriliumDataPath = `${homedir}/${dataDirName}`; mockFn.pathJoinMock.mockImplementation(() => mockTriliumDataPath); // set fs.existsSync to true, i.e. the folder does exist mockFn.existsSyncMock.mockImplementation(() => true); const result = getTriliumDataDir(dataDirName); expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(1); expect(result).toEqual(mockTriliumDataPath); }); }); describe("case C", () => { it("w/ Platform 'Linux', an existing App Data Folder (~/.local/share) but non-existing Trilium dir (~/.local/share/trilium-data) – it should attempt to create the dir", async () => { const homedir = "/home/mock"; const dataDirName = "trilium-data"; const mockPlatformDataPath = `${homedir}/.local/share/${dataDirName}`; // mock set: os.platform, os.homedir and pathJoin return values setMockPlatform("linux", homedir, mockPlatformDataPath); // use Generator to precisely control order of fs.existSync return values const existsSyncMockGen = (function* () { // 1) fs.existSync -> case B yield false; // 2) fs.existSync -> case C -> checking if default OS PlatformAppDataDir exists yield true; // 3) fs.existSync -> case C -> checking if Trilium Data folder exists yield false; })(); mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value); const result = getTriliumDataDir(dataDirName); expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(3); expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1); expect(result).toEqual(mockPlatformDataPath); }); it("w/ Platform Linux, an existing App Data Folder (~/.local/share) AND an existing Trilium Data dir – it should return path to the dir", async () => { const homedir = "/home/mock"; const dataDirName = "trilium-data"; const mockPlatformDataPath = `${homedir}/.local/share/${dataDirName}`; // mock set: os.platform, os.homedir and pathJoin return values setMockPlatform("linux", homedir, mockPlatformDataPath); // use Generator to precisely control order of fs.existSync return values const existsSyncMockGen = (function* () { // 1) fs.existSync -> case B yield false; // 2) fs.existSync -> case C -> checking if default OS PlatformAppDataDir exists yield true; // 3) fs.existSync -> case C -> checking if Trilium Data folder exists yield true; })(); mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value); const result = getTriliumDataDir(dataDirName); expect(result).toEqual(mockPlatformDataPath); expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(3); expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(0); }); it("w/ Platform 'win32' and set process.env.APPDATA behaviour", async () => { const homedir = "C:\\Users\\mock"; const dataDirName = "trilium-data"; const appDataDir = `${homedir}\\AppData\\Roaming`; const mockPlatformDataPath = `${appDataDir}\\${dataDirName}`; process.env.APPDATA = `${appDataDir}`; // mock set: os.platform, os.homedir and pathJoin return values setMockPlatform("win32", homedir, mockPlatformDataPath); // use Generator to precisely control order of fs.existSync return values const existsSyncMockGen = (function* () { // 1) fs.existSync -> case B yield false; // 2) fs.existSync -> case C -> checking if default OS PlatformAppDataDir exists yield true; // 3) fs.existSync -> case C -> checking if Trilium Data folder exists yield false; })(); mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value); const result = getTriliumDataDir(dataDirName); expect(result).toEqual(mockPlatformDataPath); expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(3); expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1); }); }); describe("case D", () => { it("w/ unknown PlatformAppDataDir it should attempt to create the folder in the homefolder", async () => { const homedir = "/home/mock"; const dataDirName = "trilium-data"; const mockPlatformDataPath = `${homedir}/${dataDirName}`; setMockPlatform("aix", homedir, mockPlatformDataPath); const existsSyncMockGen = (function* () { // first fs.existSync -> case B -> checking if folder exists in home folder yield false; // second fs.existSync -> case D -> triggered by createDirIfNotExisting yield false; })(); mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value); const result = getTriliumDataDir(dataDirName); expect(result).toEqual(mockPlatformDataPath); expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(2); expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1); }); }); }); describe("#getDataDirs()", () => { const envKeys: Omit, "TRILIUM_DATA_DIR">[] = ["DOCUMENT_PATH", "BACKUP_DIR", "LOG_DIR", "ANONYMIZED_DB_DIR", "CONFIG_INI_PATH"]; const setMockedEnv = (prefix: string | null) => { envKeys.forEach((key) => { if (prefix) { process.env[`TRILIUM_${key}`] = `${prefix}_${key}`; } else { delete process.env[`TRILIUM_${key}`]; } }); }; it("w/ process.env values present, it should return an object using values from process.env", () => { // set mocked values const mockValuePrefix = "MOCK"; setMockedEnv(mockValuePrefix); // get result const result = getDataDirs(`${mockValuePrefix}_TRILIUM_DATA_DIR`); for (const key in result) { expect(result[key]).toEqual(`${mockValuePrefix}_${key}`); } }); it("w/ NO process.env values present, it should return an object using supplied TRILIUM_DATA_DIR as base", () => { // make sure values are undefined setMockedEnv(null); // mock pathJoin implementation to just return mockDataDir const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR"; mockFn.pathJoinMock.mockImplementation(() => mockDataDir); const result = getDataDirs(mockDataDir); for (const key in result) { expect(result[key].startsWith(mockDataDir)).toBeTruthy(); } mockFn.pathJoinMock.mockReset(); }); it("should ignore attempts to change a property on the returned object", () => { // make sure values are undefined setMockedEnv(null); const mockDataDirBase = "/home/test/MOCK_TRILIUM_DATA_DIR"; const result = getDataDirs(mockDataDirBase); // as per MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#description // Any attempt to change a frozen object will, either silently be ignored or // throw a TypeError exception (most commonly, but not exclusively, when in strict mode). // so be safe and check for both, even though it looks weird const getChangeAttemptResult = () => { try { //@ts-expect-error - attempt to change value of readonly property result.BACKUP_DIR = "attempt to change"; return result.BACKUP_DIR; } catch (error) { return error; } }; const changeAttemptResult = getChangeAttemptResult(); if (typeof changeAttemptResult === "string") { // if it didn't throw above: assert that it did not change the value of it or any other keys of the object for (const key in result) { expect(result[key].startsWith(mockDataDirBase)).toBeTruthy(); } } else { expect(changeAttemptResult).toBeInstanceOf(TypeError); } }); }); });