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:42:55 +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 ( ) = > {
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 ) ;
} ;
2025-01-03 22:08:56 +01:00
2025-01-04 00:16:26 +01:00
describe ( "#getPlatformAppDataDir()" , ( ) = > {
2025-01-03 22:08:56 +01:00
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
] ,
[
"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 ( )
} )
} )
} )
2025-01-18 12:42:55 +01:00
describe ( "#getTriliumDataDir" , async ( ) = > {
beforeEach ( ( ) = > {
// make sure these are not set
delete process . env . TRILIUM_DATA_DIR ;
delete process . env . APPDATA ;
resetAllMocks ( ) ;
} ) ;
describe ( "case A – process.env.TRILIUM_DATA_DIR is set" , ( ) = > {
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 – process.env.TRILIUM_DATA_DIR is not set and Trilium folder is existing in platform's home dir" , ( ) = > {
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 – process.env.TRILIUM_DATA_DIR is not set and Trilium folder is not existing in platform's home dir" , ( ) = > {
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 – fallback to creating Trilium folder in home dir" , ( ) = > {
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 ) ;
} ) ;
} ) ;
2025-01-03 22:08:56 +01:00
} )
2025-01-04 00:16:26 +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 } ` )
}
} )
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-18 12:42:55 +01:00
// mock pathJoin implementation to just return mockDataDir
const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR" ;
mockFn . pathJoinMock . mockImplementation ( ( ) = > mockDataDir ) ;
2025-01-04 00:16:26 +01:00
const result = getDataDirs ( mockDataDir ) ;
for ( const key in result ) {
expect ( result [ key ] . startsWith ( mockDataDir ) ) . toBeTruthy ( )
}
2025-01-18 12:42:55 +01:00
mockFn . pathJoinMock . mockReset ( )
2025-01-04 00:16:26 +01:00
} )
it ( "should ignore attempts to change a property on the returned object" , ( ) = > {
// make sure values are undefined
setMockedEnv ( null ) ;
2025-01-17 19:26:47 +01:00
const mockDataDirBase = "/home/test/MOCK_TRILIUM_DATA_DIR"
const result = getDataDirs ( mockDataDirBase ) ;
2025-01-04 00:16:26 +01:00
2025-01-17 19:26:47 +01:00
// 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
2025-01-04 00:16:26 +01:00
2025-01-17 19:26:47 +01:00
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-04 00:16:26 +01:00
}
2025-01-17 19:26:47 +01:00
2025-01-04 00:16:26 +01:00
} )
2025-01-03 22:08:56 +01:00
} )
} ) ;