1+ import { vi , describe , it , expect , beforeEach , afterEach } from "vitest" ;
2+ import fs from "fs" ;
3+ import ini from "ini" ;
4+
5+ // Mock dependencies
6+ vi . mock ( "fs" ) ;
7+ vi . mock ( "./data_dir.js" , ( ) => ( {
8+ default : {
9+ CONFIG_INI_PATH : "/test/config.ini"
10+ }
11+ } ) ) ;
12+ vi . mock ( "./resource_dir.js" , ( ) => ( {
13+ default : {
14+ RESOURCE_DIR : "/test/resources"
15+ }
16+ } ) ) ;
17+
18+ describe ( "Config Service" , ( ) => {
19+ let originalEnv : NodeJS . ProcessEnv ;
20+
21+ beforeEach ( ( ) => {
22+ // Save original environment
23+ originalEnv = { ...process . env } ;
24+
25+ // Clear all TRILIUM env vars
26+ Object . keys ( process . env ) . forEach ( key => {
27+ if ( key . startsWith ( "TRILIUM_" ) ) {
28+ delete process . env [ key ] ;
29+ }
30+ } ) ;
31+
32+ // Mock fs to return empty config
33+ vi . mocked ( fs . existsSync ) . mockReturnValue ( true ) ;
34+ vi . mocked ( fs . readFileSync ) . mockImplementation ( ( path ) => {
35+ if ( String ( path ) . includes ( "config-sample.ini" ) ) {
36+ return "" as any ; // Return string for INI parsing
37+ }
38+ // Return empty INI config as string
39+ return `
40+ [General]
41+ [Network]
42+ [Session]
43+ [Sync]
44+ [MultiFactorAuthentication]
45+ [Logging]
46+ ` as any ;
47+ } ) ;
48+
49+ // Clear module cache to reload config with new env vars
50+ vi . resetModules ( ) ;
51+ } ) ;
52+
53+ afterEach ( ( ) => {
54+ // Restore original environment
55+ process . env = originalEnv ;
56+ vi . clearAllMocks ( ) ;
57+ } ) ;
58+
59+ describe ( "Environment Variable Naming" , ( ) => {
60+ it ( "should use standard environment variables following TRILIUM_[SECTION]_[KEY] pattern" , async ( ) => {
61+ // Set standard env vars
62+ process . env . TRILIUM_GENERAL_INSTANCENAME = "test-instance" ;
63+ process . env . TRILIUM_NETWORK_CORSALLOWORIGIN = "https://example.com" ;
64+ process . env . TRILIUM_SYNC_SYNCSERVERHOST = "sync.example.com" ;
65+ process . env . TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL = "https://auth.example.com" ;
66+ process . env . TRILIUM_LOGGING_RETENTIONDAYS = "30" ;
67+
68+ const { default : config } = await import ( "./config.js" ) ;
69+
70+ expect ( config . General . instanceName ) . toBe ( "test-instance" ) ;
71+ expect ( config . Network . corsAllowOrigin ) . toBe ( "https://example.com" ) ;
72+ expect ( config . Sync . syncServerHost ) . toBe ( "sync.example.com" ) ;
73+ expect ( config . MultiFactorAuthentication . oauthBaseUrl ) . toBe ( "https://auth.example.com" ) ;
74+ expect ( config . Logging . retentionDays ) . toBe ( 30 ) ;
75+ } ) ;
76+
77+ it ( "should maintain backward compatibility with alias environment variables" , async ( ) => {
78+ // Set alias/legacy env vars
79+ process . env . TRILIUM_NETWORK_CORS_ALLOW_ORIGIN = "https://legacy.com" ;
80+ process . env . TRILIUM_SYNC_SERVER_HOST = "legacy-sync.com" ;
81+ process . env . TRILIUM_OAUTH_BASE_URL = "https://legacy-auth.com" ;
82+ process . env . TRILIUM_LOGGING_RETENTION_DAYS = "60" ;
83+
84+ const { default : config } = await import ( "./config.js" ) ;
85+
86+ expect ( config . Network . corsAllowOrigin ) . toBe ( "https://legacy.com" ) ;
87+ expect ( config . Sync . syncServerHost ) . toBe ( "legacy-sync.com" ) ;
88+ expect ( config . MultiFactorAuthentication . oauthBaseUrl ) . toBe ( "https://legacy-auth.com" ) ;
89+ expect ( config . Logging . retentionDays ) . toBe ( 60 ) ;
90+ } ) ;
91+
92+ it ( "should prioritize standard env vars over aliases when both are set" , async ( ) => {
93+ // Set both standard and alias env vars - standard should win
94+ process . env . TRILIUM_NETWORK_CORSALLOWORIGIN = "standard-cors.com" ;
95+ process . env . TRILIUM_NETWORK_CORS_ALLOW_ORIGIN = "alias-cors.com" ;
96+
97+ process . env . TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL = "standard-auth.com" ;
98+ process . env . TRILIUM_OAUTH_BASE_URL = "alias-auth.com" ;
99+
100+ const { default : config } = await import ( "./config.js" ) ;
101+
102+ expect ( config . Network . corsAllowOrigin ) . toBe ( "standard-cors.com" ) ;
103+ expect ( config . MultiFactorAuthentication . oauthBaseUrl ) . toBe ( "standard-auth.com" ) ;
104+ } ) ;
105+
106+ it ( "should handle all CORS environment variables correctly" , async ( ) => {
107+ // Test with standard naming
108+ process . env . TRILIUM_NETWORK_CORSALLOWORIGIN = "*" ;
109+ process . env . TRILIUM_NETWORK_CORSALLOWMETHODS = "GET,POST,PUT" ;
110+ process . env . TRILIUM_NETWORK_CORSALLOWHEADERS = "Content-Type,Authorization" ;
111+
112+ let { default : config } = await import ( "./config.js" ) ;
113+
114+ expect ( config . Network . corsAllowOrigin ) . toBe ( "*" ) ;
115+ expect ( config . Network . corsAllowMethods ) . toBe ( "GET,POST,PUT" ) ;
116+ expect ( config . Network . corsAllowHeaders ) . toBe ( "Content-Type,Authorization" ) ;
117+
118+ // Clear and test with alias naming
119+ delete process . env . TRILIUM_NETWORK_CORSALLOWORIGIN ;
120+ delete process . env . TRILIUM_NETWORK_CORSALLOWMETHODS ;
121+ delete process . env . TRILIUM_NETWORK_CORSALLOWHEADERS ;
122+
123+ process . env . TRILIUM_NETWORK_CORS_ALLOW_ORIGIN = "https://app.com" ;
124+ process . env . TRILIUM_NETWORK_CORS_ALLOW_METHODS = "GET,POST" ;
125+ process . env . TRILIUM_NETWORK_CORS_ALLOW_HEADERS = "X-Custom-Header" ;
126+
127+ vi . resetModules ( ) ;
128+ config = ( await import ( "./config.js" ) ) . default ;
129+
130+ expect ( config . Network . corsAllowOrigin ) . toBe ( "https://app.com" ) ;
131+ expect ( config . Network . corsAllowMethods ) . toBe ( "GET,POST" ) ;
132+ expect ( config . Network . corsAllowHeaders ) . toBe ( "X-Custom-Header" ) ;
133+ } ) ;
134+
135+ it ( "should handle all OAuth/MFA environment variables correctly" , async ( ) => {
136+ // Test with standard naming
137+ process . env . TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL = "https://oauth.standard.com" ;
138+ process . env . TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID = "standard-client-id" ;
139+ process . env . TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET = "standard-secret" ;
140+ process . env . TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL = "https://issuer.standard.com" ;
141+ process . env . TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME = "Standard Auth" ;
142+ process . env . TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON = "standard-icon.png" ;
143+
144+ let { default : config } = await import ( "./config.js" ) ;
145+
146+ expect ( config . MultiFactorAuthentication . oauthBaseUrl ) . toBe ( "https://oauth.standard.com" ) ;
147+ expect ( config . MultiFactorAuthentication . oauthClientId ) . toBe ( "standard-client-id" ) ;
148+ expect ( config . MultiFactorAuthentication . oauthClientSecret ) . toBe ( "standard-secret" ) ;
149+ expect ( config . MultiFactorAuthentication . oauthIssuerBaseUrl ) . toBe ( "https://issuer.standard.com" ) ;
150+ expect ( config . MultiFactorAuthentication . oauthIssuerName ) . toBe ( "Standard Auth" ) ;
151+ expect ( config . MultiFactorAuthentication . oauthIssuerIcon ) . toBe ( "standard-icon.png" ) ;
152+
153+ // Clear and test with alias naming
154+ Object . keys ( process . env ) . forEach ( key => {
155+ if ( key . startsWith ( "TRILIUM_MULTIFACTORAUTHENTICATION_" ) ) {
156+ delete process . env [ key ] ;
157+ }
158+ } ) ;
159+
160+ process . env . TRILIUM_OAUTH_BASE_URL = "https://oauth.alias.com" ;
161+ process . env . TRILIUM_OAUTH_CLIENT_ID = "alias-client-id" ;
162+ process . env . TRILIUM_OAUTH_CLIENT_SECRET = "alias-secret" ;
163+ process . env . TRILIUM_OAUTH_ISSUER_BASE_URL = "https://issuer.alias.com" ;
164+ process . env . TRILIUM_OAUTH_ISSUER_NAME = "Alias Auth" ;
165+ process . env . TRILIUM_OAUTH_ISSUER_ICON = "alias-icon.png" ;
166+
167+ vi . resetModules ( ) ;
168+ config = ( await import ( "./config.js" ) ) . default ;
169+
170+ expect ( config . MultiFactorAuthentication . oauthBaseUrl ) . toBe ( "https://oauth.alias.com" ) ;
171+ expect ( config . MultiFactorAuthentication . oauthClientId ) . toBe ( "alias-client-id" ) ;
172+ expect ( config . MultiFactorAuthentication . oauthClientSecret ) . toBe ( "alias-secret" ) ;
173+ expect ( config . MultiFactorAuthentication . oauthIssuerBaseUrl ) . toBe ( "https://issuer.alias.com" ) ;
174+ expect ( config . MultiFactorAuthentication . oauthIssuerName ) . toBe ( "Alias Auth" ) ;
175+ expect ( config . MultiFactorAuthentication . oauthIssuerIcon ) . toBe ( "alias-icon.png" ) ;
176+ } ) ;
177+
178+ it ( "should handle all Sync environment variables correctly" , async ( ) => {
179+ // Test with standard naming
180+ process . env . TRILIUM_SYNC_SYNCSERVERHOST = "sync-standard.com" ;
181+ process . env . TRILIUM_SYNC_SYNCSERVERTIMEOUT = "60000" ;
182+ process . env . TRILIUM_SYNC_SYNCPROXY = "proxy-standard.com" ;
183+
184+ let { default : config } = await import ( "./config.js" ) ;
185+
186+ expect ( config . Sync . syncServerHost ) . toBe ( "sync-standard.com" ) ;
187+ expect ( config . Sync . syncServerTimeout ) . toBe ( "60000" ) ;
188+ expect ( config . Sync . syncProxy ) . toBe ( "proxy-standard.com" ) ;
189+
190+ // Clear and test with alias naming
191+ delete process . env . TRILIUM_SYNC_SYNCSERVERHOST ;
192+ delete process . env . TRILIUM_SYNC_SYNCSERVERTIMEOUT ;
193+ delete process . env . TRILIUM_SYNC_SYNCPROXY ;
194+
195+ process . env . TRILIUM_SYNC_SERVER_HOST = "sync-alias.com" ;
196+ process . env . TRILIUM_SYNC_SERVER_TIMEOUT = "30000" ;
197+ process . env . TRILIUM_SYNC_SERVER_PROXY = "proxy-alias.com" ;
198+
199+ vi . resetModules ( ) ;
200+ config = ( await import ( "./config.js" ) ) . default ;
201+
202+ expect ( config . Sync . syncServerHost ) . toBe ( "sync-alias.com" ) ;
203+ expect ( config . Sync . syncServerTimeout ) . toBe ( "30000" ) ;
204+ expect ( config . Sync . syncProxy ) . toBe ( "proxy-alias.com" ) ;
205+ } ) ;
206+ } ) ;
207+
208+ describe ( "INI Config Integration" , ( ) => {
209+ it ( "should fall back to INI config when no env vars are set" , async ( ) => {
210+ // Mock INI config with values
211+ vi . mocked ( fs . readFileSync ) . mockImplementation ( ( path ) => {
212+ if ( String ( path ) . includes ( "config-sample.ini" ) ) {
213+ return "" as any ;
214+ }
215+ return `
216+ [General]
217+ instanceName=ini-instance
218+
219+ [Network]
220+ corsAllowOrigin=https://ini-cors.com
221+ port=9000
222+
223+ [Sync]
224+ syncServerHost=ini-sync.com
225+
226+ [MultiFactorAuthentication]
227+ oauthBaseUrl=https://ini-oauth.com
228+
229+ [Logging]
230+ retentionDays=45
231+ ` as any ;
232+ } ) ;
233+
234+ const { default : config } = await import ( "./config.js" ) ;
235+
236+ expect ( config . General . instanceName ) . toBe ( "ini-instance" ) ;
237+ expect ( config . Network . corsAllowOrigin ) . toBe ( "https://ini-cors.com" ) ;
238+ expect ( config . Network . port ) . toBe ( "9000" ) ;
239+ expect ( config . Sync . syncServerHost ) . toBe ( "ini-sync.com" ) ;
240+ expect ( config . MultiFactorAuthentication . oauthBaseUrl ) . toBe ( "https://ini-oauth.com" ) ;
241+ expect ( config . Logging . retentionDays ) . toBe ( 45 ) ;
242+ } ) ;
243+
244+ it ( "should prioritize env vars over INI config" , async ( ) => {
245+ // Mock INI config with values
246+ vi . mocked ( fs . readFileSync ) . mockImplementation ( ( path ) => {
247+ if ( String ( path ) . includes ( "config-sample.ini" ) ) {
248+ return "" as any ;
249+ }
250+ return `
251+ [General]
252+ instanceName=ini-instance
253+
254+ [Network]
255+ corsAllowOrigin=https://ini-cors.com
256+ ` as any ;
257+ } ) ;
258+
259+ // Set env vars that should override INI
260+ process . env . TRILIUM_GENERAL_INSTANCENAME = "env-instance" ;
261+ process . env . TRILIUM_NETWORK_CORS_ALLOW_ORIGIN = "https://env-cors.com" ; // Using alias
262+
263+ const { default : config } = await import ( "./config.js" ) ;
264+
265+ expect ( config . General . instanceName ) . toBe ( "env-instance" ) ;
266+ expect ( config . Network . corsAllowOrigin ) . toBe ( "https://env-cors.com" ) ;
267+ } ) ;
268+ } ) ;
269+
270+ describe ( "Type Transformations" , ( ) => {
271+ it ( "should correctly transform boolean values" , async ( ) => {
272+ process . env . TRILIUM_GENERAL_NOAUTHENTICATION = "true" ;
273+ process . env . TRILIUM_GENERAL_NOBACKUP = "1" ;
274+ process . env . TRILIUM_GENERAL_READONLY = "false" ;
275+ process . env . TRILIUM_NETWORK_HTTPS = "0" ;
276+
277+ const { default : config } = await import ( "./config.js" ) ;
278+
279+ expect ( config . General . noAuthentication ) . toBe ( true ) ;
280+ expect ( config . General . noBackup ) . toBe ( true ) ;
281+ expect ( config . General . readOnly ) . toBe ( false ) ;
282+ expect ( config . Network . https ) . toBe ( false ) ;
283+ } ) ;
284+
285+ it ( "should correctly transform integer values" , async ( ) => {
286+ process . env . TRILIUM_SESSION_COOKIEMAXAGE = "3600" ;
287+ process . env . TRILIUM_LOGGING_RETENTIONDAYS = "7" ;
288+
289+ const { default : config } = await import ( "./config.js" ) ;
290+
291+ expect ( config . Session . cookieMaxAge ) . toBe ( 3600 ) ;
292+ expect ( config . Logging . retentionDays ) . toBe ( 7 ) ;
293+ } ) ;
294+
295+ it ( "should use default values for invalid integers" , async ( ) => {
296+ process . env . TRILIUM_SESSION_COOKIEMAXAGE = "invalid" ;
297+ process . env . TRILIUM_LOGGING_RETENTION_DAYS = "not-a-number" ; // Using alias
298+
299+ const { default : config } = await import ( "./config.js" ) ;
300+
301+ expect ( config . Session . cookieMaxAge ) . toBe ( 21 * 24 * 60 * 60 ) ; // Default
302+ expect ( config . Logging . retentionDays ) . toBe ( 90 ) ; // Default
303+ } ) ;
304+ } ) ;
305+
306+ describe ( "Default Values" , ( ) => {
307+ it ( "should use correct default values when no config is provided" , async ( ) => {
308+ const { default : config } = await import ( "./config.js" ) ;
309+
310+ // General defaults
311+ expect ( config . General . instanceName ) . toBe ( "" ) ;
312+ expect ( config . General . noAuthentication ) . toBe ( false ) ;
313+ expect ( config . General . noBackup ) . toBe ( false ) ;
314+ expect ( config . General . noDesktopIcon ) . toBe ( false ) ;
315+ expect ( config . General . readOnly ) . toBe ( false ) ;
316+
317+ // Network defaults
318+ expect ( config . Network . host ) . toBe ( "0.0.0.0" ) ;
319+ expect ( config . Network . port ) . toBe ( "3000" ) ;
320+ expect ( config . Network . https ) . toBe ( false ) ;
321+ expect ( config . Network . certPath ) . toBe ( "" ) ;
322+ expect ( config . Network . keyPath ) . toBe ( "" ) ;
323+ expect ( config . Network . trustedReverseProxy ) . toBe ( false ) ;
324+ expect ( config . Network . corsAllowOrigin ) . toBe ( "" ) ;
325+ expect ( config . Network . corsAllowMethods ) . toBe ( "" ) ;
326+ expect ( config . Network . corsAllowHeaders ) . toBe ( "" ) ;
327+
328+ // Session defaults
329+ expect ( config . Session . cookieMaxAge ) . toBe ( 21 * 24 * 60 * 60 ) ;
330+
331+ // Sync defaults
332+ expect ( config . Sync . syncServerHost ) . toBe ( "" ) ;
333+ expect ( config . Sync . syncServerTimeout ) . toBe ( "120000" ) ;
334+ expect ( config . Sync . syncProxy ) . toBe ( "" ) ;
335+
336+ // OAuth defaults
337+ expect ( config . MultiFactorAuthentication . oauthBaseUrl ) . toBe ( "" ) ;
338+ expect ( config . MultiFactorAuthentication . oauthClientId ) . toBe ( "" ) ;
339+ expect ( config . MultiFactorAuthentication . oauthClientSecret ) . toBe ( "" ) ;
340+ expect ( config . MultiFactorAuthentication . oauthIssuerBaseUrl ) . toBe ( "https://accounts.google.com" ) ;
341+ expect ( config . MultiFactorAuthentication . oauthIssuerName ) . toBe ( "Google" ) ;
342+ expect ( config . MultiFactorAuthentication . oauthIssuerIcon ) . toBe ( "" ) ;
343+
344+ // Logging defaults
345+ expect ( config . Logging . retentionDays ) . toBe ( 90 ) ;
346+ } ) ;
347+ } ) ;
348+ } ) ;
0 commit comments