2025-01-18 12:42:55 +01:00
import { describe , it , expect , beforeEach , vi } from "vitest" ;
2025-01-03 22:08:56 +01:00
2025-01-18 12:48:05 +01:00
import type { getTriliumDataDir as getTriliumDataDirType , getDataDirs as getDataDirsType , getPlatformAppDataDir as getPlatformAppDataDirType } from "../src/services/data_dir" ;
2025-01-03 22:08:56 +01:00
2025-01-18 12:42:55 +01:00
describe ( "data_dir.ts unit tests" , async ( ) = > {
2025-01-18 12:48:05 +01:00
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 ( )
} ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
// 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
}
} ;
2025-01-18 12:42:55 +01:00
} ) ;
2025-01-18 12:48:05 +01:00
vi . doMock ( "node:os" , ( ) = > {
return {
default : {
homedir : mockFn.osHomedirMock ,
platform : mockFn.osPlatformMock
}
} ;
} ) ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
vi . doMock ( "path" , ( ) = > {
return {
join : mockFn.pathJoinMock
} ;
} ) ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
// 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" ) ) ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
// helper to reset call counts
const resetAllMocks = ( ) = > {
Object . values ( mockFn ) . forEach ( ( mockedFn ) = > {
mockedFn . mockReset ( ) ;
2025-01-18 12:42:55 +01:00
} ) ;
2025-01-18 12:48:05 +01:00
} ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
// 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 ) ;
} ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
describe ( "#getPlatformAppDataDir()" , ( ) = > {
type TestCaseGetPlatformAppDataDir = [ description : string , fnValue : Parameters < typeof getPlatformAppDataDir > , expectedValueFn : ( val : ReturnType < typeof getPlatformAppDataDir > ) = > boolean ] ;
const testCases : TestCaseGetPlatformAppDataDir [ ] = [
[ "w/ unsupported OS it should return 'null'" , [ "aix" , undefined ] , ( val ) = > val === null ] ,
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
[ "w/ win32 and no APPDATA set it should return 'null'" , [ "win32" , undefined ] , ( val ) = > val === null ] ,
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
[ "w/ win32 and set APPDATA it should return set 'APPDATA'" , [ "win32" , "AppData" ] , ( val ) = > val === "AppData" ] ,
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
[ "w/ linux it should return '/.local/share'" , [ "linux" , undefined ] , ( val ) = > val !== null && val . endsWith ( "/.local/share" ) ] ,
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
[ "w/ linux and wrongly set APPDATA it should ignore APPDATA and return /.local/share" , [ "linux" , "FakeAppData" ] , ( val ) = > val !== null && val . endsWith ( "/.local/share" ) ] ,
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
[ "w/ darwin it should return /Library/Application Support" , [ "darwin" , undefined ] , ( val ) = > val !== null && val . endsWith ( "/Library/Application Support" ) ]
] ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
testCases . forEach ( ( testCase ) = > {
const [ testDescription , value , isExpected ] = testCase ;
return it ( testDescription , ( ) = > {
const actual = getPlatformAppDataDir ( . . . value ) ;
const result = isExpected ( actual ) ;
expect ( result ) . toBeTruthy ( ) ;
} ) ;
2025-01-18 12:42:55 +01:00
} ) ;
} ) ;
2025-01-18 12:48:05 +01:00
describe ( "#getTriliumDataDir" , async ( ) = > {
beforeEach ( ( ) = > {
// make sure these are not set
delete process . env . TRILIUM_DATA_DIR ;
delete process . env . APPDATA ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
resetAllMocks ( ) ;
2025-01-18 12:42:55 +01:00
} ) ;
2025-01-18 12:48:05 +01:00
/ * *
* 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 ) ;
} ) ;
} ) ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
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 } ` ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
mockFn . pathJoinMock . mockImplementation ( ( ) = > mockTriliumDataPath ) ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
// set fs.existsSync to true, i.e. the folder does exist
mockFn . existsSyncMock . mockImplementation ( ( ) = > true ) ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
const result = getTriliumDataDir ( dataDirName ) ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
expect ( mockFn . existsSyncMock ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( result ) . toEqual ( mockTriliumDataPath ) ;
} ) ;
2025-01-18 12:42:55 +01:00
} ) ;
2025-01-18 12:48:05 +01:00
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 ) ;
} ) ;
2025-01-18 12:42:55 +01:00
} ) ;
2025-01-18 12:48:05 +01:00
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 } ` ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
setMockPlatform ( "aix" , homedir , mockPlatformDataPath ) ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
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 ;
} ) ( ) ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
mockFn . existsSyncMock . mockImplementation ( ( ) = > existsSyncMockGen . next ( ) . value ) ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
const result = getTriliumDataDir ( dataDirName ) ;
2025-01-18 12:42:55 +01:00
2025-01-18 12:48:05 +01:00
expect ( result ) . toEqual ( mockPlatformDataPath ) ;
expect ( mockFn . existsSyncMock ) . toHaveBeenCalledTimes ( 2 ) ;
expect ( mockFn . mkdirSyncMock ) . toHaveBeenCalledTimes ( 1 ) ;
} ) ;
2025-01-18 12:42:55 +01:00
} ) ;
} ) ;
2025-01-04 00:16:26 +01:00
2025-01-18 12:48:05 +01:00
describe ( "#getDataDirs()" , ( ) = > {
const envKeys : Omit < keyof ReturnType < typeof getDataDirs > , "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 } ` ) ;
}
} ) ;
2025-01-04 00:16:26 +01:00
2025-01-18 12:48:05 +01:00
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 ) ;
2025-01-04 00:16:26 +01:00
2025-01-18 12:48:05 +01:00
// mock pathJoin implementation to just return mockDataDir
const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR" ;
mockFn . pathJoinMock . mockImplementation ( ( ) = > mockDataDir ) ;
2025-01-17 19:26:47 +01:00
2025-01-18 12:48:05 +01:00
const result = getDataDirs ( mockDataDir ) ;
2025-01-17 19:26:47 +01:00
2025-01-18 12:48:05 +01:00
for ( const key in result ) {
expect ( result [ key ] . startsWith ( mockDataDir ) ) . toBeTruthy ( ) ;
}
2025-01-17 19:26:47 +01:00
2025-01-18 12:48:05 +01:00
mockFn . pathJoinMock . mockReset ( ) ;
} ) ;
2025-01-03 22:08:56 +01:00
2025-01-18 12:48:05 +01:00
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 ) ;
}
} ) ;
} ) ;
2025-01-03 22:08:56 +01:00
} ) ;