11import { Logger } from '@nestjs/common' ;
22import { Test , TestingModule } from '@nestjs/testing' ;
3+ import { mkdtemp , readFile , rm , writeFile } from 'fs/promises' ;
4+ import { tmpdir } from 'os' ;
5+ import { join } from 'path' ;
36
4- import { beforeEach , describe , expect , it , vi } from 'vitest' ;
7+ import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest' ;
58
69import {
710 UPSCableType ,
@@ -14,18 +17,18 @@ import { UPSService } from '@app/unraid-api/graph/resolvers/ups/ups.service.js';
1417
1518// Mock dependencies
1619vi . mock ( 'execa' ) ;
17- vi . mock ( 'fs/promises' ) ;
1820vi . mock ( '@app/core/utils/files/file-exists.js' ) ;
1921
2022const mockExeca = vi . mocked ( ( await import ( 'execa' ) ) . execa ) ;
21- const mockReadFile = vi . mocked ( ( await import ( 'fs/promises' ) ) . readFile ) ;
22- const mockWriteFile = vi . mocked ( ( await import ( 'fs/promises' ) ) . writeFile ) ;
2323const mockFileExistsSync = vi . mocked (
2424 ( await import ( '@app/core/utils/files/file-exists.js' ) ) . fileExistsSync
2525) ;
2626
2727describe ( 'UPSService' , ( ) => {
2828 let service : UPSService ;
29+ let tempDir : string ;
30+ let configPath : string ;
31+ let backupPath : string ;
2932
3033 const mockCurrentConfig = {
3134 SERVICE : 'disable' ,
@@ -42,28 +45,61 @@ describe('UPSService', () => {
4245 MODELNAME : 'APC UPS' ,
4346 } ;
4447
48+ // Helper to create config file content
49+ const createConfigContent = ( config : Record < string , any > ) : string => {
50+ const lines = [ '# APC UPS Configuration File' ] ;
51+ for ( const [ key , value ] of Object . entries ( config ) ) {
52+ if ( value !== undefined ) {
53+ lines . push (
54+ `${ key } ${ typeof value === 'string' && value . includes ( ' ' ) ? `"${ value } "` : value } `
55+ ) ;
56+ }
57+ }
58+ return lines . join ( '\n' ) + '\n' ;
59+ } ;
60+
4561 beforeEach ( async ( ) => {
4662 vi . clearAllMocks ( ) ;
4763
64+ // Create temporary directory for test files
65+ tempDir = await mkdtemp ( join ( tmpdir ( ) , 'ups-test-' ) ) ;
66+ configPath = join ( tempDir , 'apcupsd.conf' ) ;
67+ backupPath = `${ configPath } .backup` ;
68+
4869 const module : TestingModule = await Test . createTestingModule ( {
4970 providers : [ UPSService ] ,
5071 } ) . compile ( ) ;
5172
5273 service = module . get < UPSService > ( UPSService ) ;
5374
75+ // Override the config path to use our temp directory
76+ Object . defineProperty ( service , 'configPath' , {
77+ value : configPath ,
78+ writable : false ,
79+ configurable : true ,
80+ } ) ;
81+
5482 // Mock logger methods on the service instance
5583 vi . spyOn ( service [ 'logger' ] , 'debug' ) . mockImplementation ( ( ) => { } ) ;
5684 vi . spyOn ( service [ 'logger' ] , 'warn' ) . mockImplementation ( ( ) => { } ) ;
5785 vi . spyOn ( service [ 'logger' ] , 'error' ) . mockImplementation ( ( ) => { } ) ;
5886
5987 // Default mocks
60- mockFileExistsSync . mockReturnValue ( true ) ;
61- mockReadFile . mockResolvedValue ( 'SERVICE disable\nUPSCABLE usb\nUPSTYPE usb\n' ) ;
62- mockWriteFile . mockResolvedValue ( undefined ) ;
88+ mockFileExistsSync . mockImplementation ( ( path ) => {
89+ if ( path === configPath ) {
90+ return true ;
91+ }
92+ return false ;
93+ } ) ;
6394 mockExeca . mockResolvedValue ( { stdout : '' , stderr : '' , exitCode : 0 } as any ) ;
6495
65- // Mock getCurrentConfig to return our test config
66- vi . spyOn ( service , 'getCurrentConfig' ) . mockResolvedValue ( mockCurrentConfig ) ;
96+ // Create initial config file
97+ await writeFile ( configPath , createConfigContent ( mockCurrentConfig ) ) ;
98+ } ) ;
99+
100+ afterEach ( async ( ) => {
101+ // Clean up temp directory
102+ await rm ( tempDir , { recursive : true , force : true } ) ;
67103 } ) ;
68104
69105 describe ( 'configureUPS' , ( ) => {
@@ -75,54 +111,34 @@ describe('UPSService', () => {
75111
76112 await service . configureUPS ( partialConfig ) ;
77113
114+ // Read the written config file
115+ const writtenConfig = await readFile ( configPath , 'utf-8' ) ;
116+
78117 // Should preserve existing values for fields not provided
79- expect ( mockWriteFile ) . toHaveBeenCalledWith (
80- '/etc/apcupsd/apcupsd.conf' ,
81- expect . stringContaining ( 'SERVICE disable' ) , // preserved from existing
82- 'utf-8'
83- ) ;
84- expect ( mockWriteFile ) . toHaveBeenCalledWith (
85- '/etc/apcupsd/apcupsd.conf' ,
86- expect . stringContaining ( 'UPSTYPE usb' ) , // preserved from existing
87- 'utf-8'
88- ) ;
89- expect ( mockWriteFile ) . toHaveBeenCalledWith (
90- '/etc/apcupsd/apcupsd.conf' ,
91- expect . stringContaining ( 'BATTERYLEVEL 25' ) , // updated value
92- 'utf-8'
93- ) ;
94- expect ( mockWriteFile ) . toHaveBeenCalledWith (
95- '/etc/apcupsd/apcupsd.conf' ,
96- expect . stringContaining ( 'MINUTES 10' ) , // updated value
97- 'utf-8'
98- ) ;
118+ expect ( writtenConfig ) . toContain ( 'SERVICE disable' ) ; // preserved from existing
119+ expect ( writtenConfig ) . toContain ( 'UPSTYPE usb' ) ; // preserved from existing
120+ expect ( writtenConfig ) . toContain ( 'BATTERYLEVEL 25' ) ; // updated value
121+ expect ( writtenConfig ) . toContain ( 'MINUTES 10' ) ; // updated value
122+ expect ( writtenConfig ) . toContain ( 'UPSNAME MyUPS' ) ; // preserved
123+ expect ( writtenConfig ) . toContain ( 'DEVICE /dev/ttyUSB0' ) ; // preserved
99124 } ) ;
100125
101126 it ( 'should use default values when neither input nor existing config provide values' , async ( ) => {
102- // Mock getCurrentConfig to return empty config
103- vi . spyOn ( service , 'getCurrentConfig' ) . mockResolvedValue ( { } ) ;
127+ // Write empty config file
128+ await writeFile ( configPath , '# Empty config\n' ) ;
104129
105130 const partialConfig : UPSConfigInput = {
106131 service : UPSServiceState . ENABLE ,
107132 } ;
108133
109134 await service . configureUPS ( partialConfig ) ;
110135
111- expect ( mockWriteFile ) . toHaveBeenCalledWith (
112- '/etc/apcupsd/apcupsd.conf' ,
113- expect . stringContaining ( 'SERVICE enable' ) , // provided value
114- 'utf-8'
115- ) ;
116- expect ( mockWriteFile ) . toHaveBeenCalledWith (
117- '/etc/apcupsd/apcupsd.conf' ,
118- expect . stringContaining ( 'UPSTYPE usb' ) , // default value
119- 'utf-8'
120- ) ;
121- expect ( mockWriteFile ) . toHaveBeenCalledWith (
122- '/etc/apcupsd/apcupsd.conf' ,
123- expect . stringContaining ( 'BATTERYLEVEL 10' ) , // default value
124- 'utf-8'
125- ) ;
136+ const writtenConfig = await readFile ( configPath , 'utf-8' ) ;
137+
138+ expect ( writtenConfig ) . toContain ( 'SERVICE enable' ) ; // provided value
139+ expect ( writtenConfig ) . toContain ( 'UPSTYPE usb' ) ; // default value
140+ expect ( writtenConfig ) . toContain ( 'BATTERYLEVEL 10' ) ; // default value
141+ expect ( writtenConfig ) . toContain ( 'MINUTES 5' ) ; // default value
126142 } ) ;
127143
128144 it ( 'should handle custom cable configuration' , async ( ) => {
@@ -133,16 +149,19 @@ describe('UPSService', () => {
133149
134150 await service . configureUPS ( config ) ;
135151
136- expect ( mockWriteFile ) . toHaveBeenCalledWith (
137- '/etc/apcupsd/apcupsd.conf' ,
138- expect . stringContaining ( 'UPSCABLE custom-config-string' ) ,
139- 'utf-8'
140- ) ;
152+ const writtenConfig = await readFile ( configPath , 'utf-8' ) ;
153+ expect ( writtenConfig ) . toContain ( 'UPSCABLE custom-config-string' ) ;
141154 } ) ;
142155
143156 it ( 'should validate required fields after merging' , async ( ) => {
144- // Mock getCurrentConfig to return config without required fields
145- vi . spyOn ( service , 'getCurrentConfig' ) . mockResolvedValue ( { } ) ;
157+ // Write config without device field
158+ await writeFile (
159+ configPath ,
160+ createConfigContent ( {
161+ SERVICE : 'disable' ,
162+ UPSTYPE : 'usb' ,
163+ } )
164+ ) ;
146165
147166 const config : UPSConfigInput = {
148167 upsType : UPSType . NET , // requires device
@@ -238,13 +257,7 @@ describe('UPSService', () => {
238257
239258 await service . configureUPS ( config ) ;
240259
241- // Find the config file write call (not the backup)
242- const configWriteCall = mockWriteFile . mock . calls . find (
243- ( call ) => call [ 0 ] === '/etc/apcupsd/apcupsd.conf' && call [ 1 ] !== 'backup content'
244- ) ;
245- expect ( configWriteCall ) . toBeDefined ( ) ;
246-
247- const configContent = configWriteCall ! [ 1 ] as string ;
260+ const configContent = await readFile ( configPath , 'utf-8' ) ;
248261
249262 // Should preserve existing values in the generated format
250263 expect ( configContent ) . toContain ( 'UPSNAME MyUPS' ) ;
@@ -266,54 +279,49 @@ describe('UPSService', () => {
266279 } ) ;
267280
268281 it ( 'should create backup before writing new config' , async ( ) => {
282+ const originalContent = await readFile ( configPath , 'utf-8' ) ;
283+
269284 const config : UPSConfigInput = {
270285 batteryLevel : 30 ,
271286 } ;
272287
273- mockReadFile . mockResolvedValueOnce ( 'original config content' ) ;
274-
275288 await service . configureUPS ( config ) ;
276289
277- // Should create backup
278- expect ( mockWriteFile ) . toHaveBeenCalledWith (
279- '/etc/apcupsd/apcupsd.conf.backup' ,
280- 'original config content' ,
281- 'utf-8'
282- ) ;
290+ // Should create backup with original content
291+ const backupContent = await readFile ( backupPath , 'utf-8' ) ;
292+ expect ( backupContent ) . toBe ( originalContent ) ;
283293
284294 // Should write new config
285- expect ( mockWriteFile ) . toHaveBeenCalledWith (
286- '/etc/apcupsd/apcupsd.conf' ,
287- expect . any ( String ) ,
288- 'utf-8'
289- ) ;
295+ const newContent = await readFile ( configPath , 'utf-8' ) ;
296+ expect ( newContent ) . toContain ( 'BATTERYLEVEL 30' ) ;
297+ expect ( newContent ) . not . toBe ( originalContent ) ;
290298 } ) ;
291299
292300 it ( 'should handle errors gracefully and restore backup' , async ( ) => {
301+ const originalContent = await readFile ( configPath , 'utf-8' ) ;
302+
293303 const config : UPSConfigInput = {
294304 batteryLevel : 30 ,
295305 } ;
296306
297- // Mock file operations
298- mockReadFile
299- . mockResolvedValueOnce ( 'backup content' ) // for backup creation
300- . mockResolvedValueOnce ( 'backup content' ) ; // for restore
301-
302- mockWriteFile
303- . mockResolvedValueOnce ( undefined ) // succeed backup creation
304- . mockRejectedValueOnce ( new Error ( 'Write failed' ) ) // fail main config write
305- . mockResolvedValueOnce ( undefined ) ; // succeed restore
307+ // Temporarily override generateApcupsdConfig to throw error
308+ const originalGenerate = service [ 'generateApcupsdConfig' ] . bind ( service ) ;
309+ service [ 'generateApcupsdConfig' ] = vi . fn ( ) . mockImplementation ( ( ) => {
310+ throw new Error ( 'Generation failed' ) ;
311+ } ) ;
306312
313+ // Since we can't easily mock fs operations with real files,
314+ // we'll test a different error path
307315 await expect ( service . configureUPS ( config ) ) . rejects . toThrow (
308- 'Failed to configure UPS: Write failed'
316+ 'Failed to configure UPS: Generation failed'
309317 ) ;
310318
311- // Should attempt to restore backup - check the final write call
312- const writeCallArgs = mockWriteFile . mock . calls ;
313- const lastCall = writeCallArgs [ writeCallArgs . length - 1 ] ;
314- expect ( lastCall [ 0 ] ) . toBe ( '/etc/apcupsd/apcupsd.conf' ) ;
315- expect ( lastCall [ 1 ] ) . toBe ( 'backup content ') ;
316- expect ( lastCall [ 2 ] ) . toBe ( 'utf-8' ) ;
319+ // Restore original method
320+ service [ 'generateApcupsdConfig' ] = originalGenerate ;
321+
322+ // Config should remain unchanged
323+ const currentContent = await readFile ( configPath , 'utf-8 ') ;
324+ expect ( currentContent ) . toBe ( originalContent ) ;
317325 } ) ;
318326 } ) ;
319327} ) ;
0 commit comments