1- import { Logger } from '@nestjs/common' ;
21import { Test , TestingModule } from '@nestjs/testing' ;
3- import { mkdtemp , readFile , rm , writeFile } from 'fs/promises' ;
2+ import { mkdtemp , readFile , rm , unlink , writeFile } from 'fs/promises' ;
43import { tmpdir } from 'os' ;
54import { join } from 'path' ;
65
@@ -20,9 +19,7 @@ vi.mock('execa');
2019vi . mock ( '@app/core/utils/files/file-exists.js' ) ;
2120
2221const mockExeca = vi . mocked ( ( await import ( 'execa' ) ) . execa ) ;
23- const mockFileExistsSync = vi . mocked (
24- ( await import ( '@app/core/utils/files/file-exists.js' ) ) . fileExistsSync
25- ) ;
22+ const mockFileExists = vi . mocked ( ( await import ( '@app/core/utils/files/file-exists.js' ) ) . fileExists ) ;
2623
2724describe ( 'UPSService' , ( ) => {
2825 let service : UPSService ;
@@ -85,7 +82,7 @@ describe('UPSService', () => {
8582 vi . spyOn ( service [ 'logger' ] , 'error' ) . mockImplementation ( ( ) => { } ) ;
8683
8784 // Default mocks
88- mockFileExistsSync . mockImplementation ( ( path ) => {
85+ mockFileExists . mockImplementation ( async ( path ) => {
8986 if ( path === configPath ) {
9087 return true ;
9188 }
@@ -174,53 +171,51 @@ describe('UPSService', () => {
174171 } ) ;
175172
176173 it ( 'should handle killpower configuration for enable + yes' , async ( ) => {
174+ // Create a mock rc.6 file for this test
175+ const mockRc6Path = join ( tempDir , 'mock-rc.6' ) ;
176+ await writeFile ( mockRc6Path , '/sbin/poweroff' , 'utf-8' ) ;
177+ service [ 'rc6Path' ] = mockRc6Path ;
178+
179+ // Update mock to indicate rc6 file exists
180+ mockFileExists . mockImplementation ( async ( path ) => {
181+ if ( path === configPath || path === mockRc6Path ) {
182+ return true ;
183+ }
184+ return false ;
185+ } ) ;
186+
177187 const config : UPSConfigInput = {
178188 service : UPSServiceState . ENABLE ,
179189 killUps : UPSKillPower . YES ,
180190 } ;
181191
182- // Mock grep to return exit code 1 (not found)
183- mockExeca . mockImplementation ( ( ( cmd : any , args : any ) => {
184- if ( cmd === 'grep' && Array . isArray ( args ) && args . includes ( 'apccontrol' ) ) {
185- return Promise . resolve ( { stdout : '' , stderr : '' , exitCode : 1 } as any ) ;
186- }
187- return Promise . resolve ( { stdout : '' , stderr : '' , exitCode : 0 } as any ) ;
188- } ) as any ) ;
189-
190192 await service . configureUPS ( config ) ;
191193
192- // Should call sed to add killpower
193- expect ( mockExeca ) . toHaveBeenCalledWith ( 'sed' , [
194- '-i' ,
195- '-e' ,
196- 's:/sbin/poweroff:/ etc/apcupsd/apccontrol killpower; /sbin/poweroff:' ,
197- '/etc/rc.d/rc.6' ,
198- ] ) ;
194+ // Should have modified the rc.6 file
195+ const rc6Content = await readFile ( mockRc6Path , 'utf-8' ) ;
196+ expect ( rc6Content ) . toContain ( '/etc/apcupsd/apccontrol killpower; /sbin/poweroff' ) ;
197+
198+ expect ( mockExeca ) . toHaveBeenCalledWith ( '/ etc/rc.d/rc.apcupsd' , [ 'start' ] , {
199+ timeout : 10000 ,
200+ } ) ;
199201 } ) ;
200202
201203 it ( 'should handle killpower configuration for disable case' , async ( ) => {
204+ // Create a mock rc.6 file with killpower already enabled
205+ const mockRc6Path = join ( tempDir , 'mock-rc.6-2' ) ;
206+ await writeFile ( mockRc6Path , '/etc/apcupsd/apccontrol killpower; /sbin/poweroff' , 'utf-8' ) ;
207+ service [ 'rc6Path' ] = mockRc6Path ;
208+
202209 const config : UPSConfigInput = {
203210 service : UPSServiceState . DISABLE ,
204211 killUps : UPSKillPower . YES , // should be ignored since service is disabled
205212 } ;
206213
207- // Mock grep to return exit code 0 (found)
208- mockExeca . mockImplementation ( ( ( cmd : any , args : any ) => {
209- if ( cmd === 'grep' && Array . isArray ( args ) && args . includes ( 'apccontrol' ) ) {
210- return Promise . resolve ( { stdout : '' , stderr : '' , exitCode : 0 } as any ) ;
211- }
212- return Promise . resolve ( { stdout : '' , stderr : '' , exitCode : 0 } as any ) ;
213- } ) as any ) ;
214-
215214 await service . configureUPS ( config ) ;
216215
217- // Should call sed to remove killpower
218- expect ( mockExeca ) . toHaveBeenCalledWith ( 'sed' , [
219- '-i' ,
220- '-e' ,
221- 's:/etc/apcupsd/apccontrol killpower; /sbin/poweroff:/sbin/poweroff:' ,
222- '/etc/rc.d/rc.6' ,
223- ] ) ;
216+ // Should NOT have modified the rc.6 file since service is disabled
217+ const rc6Content = await readFile ( mockRc6Path , 'utf-8' ) ;
218+ expect ( rc6Content ) . toContain ( '/etc/apcupsd/apccontrol killpower; /sbin/poweroff' ) ;
224219 } ) ;
225220
226221 it ( 'should start service when enabled' , async ( ) => {
@@ -324,4 +319,144 @@ describe('UPSService', () => {
324319 expect ( currentContent ) . toBe ( originalContent ) ;
325320 } ) ;
326321 } ) ;
322+
323+ describe ( 'killpower functionality' , ( ) => {
324+ let tempRc6Path : string ;
325+
326+ beforeEach ( async ( ) => {
327+ // Create a temporary rc.6 file for testing
328+ tempRc6Path = join ( tempDir , 'rc.6' ) ;
329+
330+ // Create a mock rc.6 content
331+ const mockRc6Content = `#!/bin/sh
332+ # Shutdown script
333+ echo "Shutting down..."
334+ /sbin/poweroff
335+ exit 0
336+ ` ;
337+ await writeFile ( tempRc6Path , mockRc6Content , 'utf-8' ) ;
338+
339+ // Override the rc6Path in the service (we'll need to make it configurable)
340+ service [ 'rc6Path' ] = tempRc6Path ;
341+
342+ // Update mock to indicate rc6 file exists
343+ mockFileExists . mockImplementation ( async ( path ) => {
344+ if ( path === configPath || path === tempRc6Path ) {
345+ return true ;
346+ }
347+ return false ;
348+ } ) ;
349+ } ) ;
350+
351+ it ( 'should enable killpower when killUps=yes and service=enable' , async ( ) => {
352+ const config : UPSConfigInput = {
353+ killUps : UPSKillPower . YES ,
354+ service : UPSServiceState . ENABLE ,
355+ upsType : UPSType . USB ,
356+ } ;
357+
358+ await service . configureUPS ( config ) ;
359+
360+ const rc6Content = await readFile ( tempRc6Path , 'utf-8' ) ;
361+ expect ( rc6Content ) . toContain ( '/etc/apcupsd/apccontrol killpower; /sbin/poweroff' ) ;
362+ // The file still contains "exit 0" on a separate line
363+ expect ( rc6Content ) . toContain ( 'exit 0' ) ;
364+ } ) ;
365+
366+ it ( 'should disable killpower when killUps=no' , async ( ) => {
367+ // First enable killpower
368+ const enableConfig : UPSConfigInput = {
369+ killUps : UPSKillPower . YES ,
370+ service : UPSServiceState . ENABLE ,
371+ upsType : UPSType . USB ,
372+ } ;
373+ await service . configureUPS ( enableConfig ) ;
374+
375+ // Then disable it
376+ const disableConfig : UPSConfigInput = {
377+ killUps : UPSKillPower . NO ,
378+ service : UPSServiceState . ENABLE ,
379+ upsType : UPSType . USB ,
380+ } ;
381+ await service . configureUPS ( disableConfig ) ;
382+
383+ const rc6Content = await readFile ( tempRc6Path , 'utf-8' ) ;
384+ expect ( rc6Content ) . not . toContain ( 'apccontrol killpower' ) ;
385+ expect ( rc6Content ) . toContain ( '/sbin/poweroff\nexit 0' ) ; // Should be restored
386+ } ) ;
387+
388+ it ( 'should not enable killpower when service=disable' , async ( ) => {
389+ const config : UPSConfigInput = {
390+ killUps : UPSKillPower . YES ,
391+ service : UPSServiceState . DISABLE , // Service is disabled
392+ upsType : UPSType . USB ,
393+ } ;
394+
395+ await service . configureUPS ( config ) ;
396+
397+ const rc6Content = await readFile ( tempRc6Path , 'utf-8' ) ;
398+ expect ( rc6Content ) . not . toContain ( 'apccontrol killpower' ) ;
399+ } ) ;
400+
401+ it ( 'should handle missing rc.6 file gracefully' , async ( ) => {
402+ // Remove the file
403+ await unlink ( tempRc6Path ) ;
404+
405+ // Update mock to indicate rc6 file does NOT exist
406+ mockFileExists . mockImplementation ( async ( path ) => {
407+ if ( path === configPath ) {
408+ return true ;
409+ }
410+ return false ;
411+ } ) ;
412+
413+ const config : UPSConfigInput = {
414+ killUps : UPSKillPower . YES ,
415+ service : UPSServiceState . ENABLE ,
416+ upsType : UPSType . USB ,
417+ } ;
418+
419+ // Should not throw - just skip killpower configuration
420+ await expect ( service . configureUPS ( config ) ) . resolves . not . toThrow ( ) ;
421+ } ) ;
422+
423+ it ( 'should be idempotent - enabling killpower multiple times' , async ( ) => {
424+ const config : UPSConfigInput = {
425+ killUps : UPSKillPower . YES ,
426+ service : UPSServiceState . ENABLE ,
427+ upsType : UPSType . USB ,
428+ } ;
429+
430+ // Enable killpower twice
431+ await service . configureUPS ( config ) ;
432+ const firstContent = await readFile ( tempRc6Path , 'utf-8' ) ;
433+
434+ await service . configureUPS ( config ) ;
435+ const secondContent = await readFile ( tempRc6Path , 'utf-8' ) ;
436+
437+ // Content should be the same after second run
438+ expect ( firstContent ) . toBe ( secondContent ) ;
439+ // Should only have one instance of killpower
440+ expect ( ( secondContent . match ( / a p c c o n t r o l k i l l p o w e r / g) || [ ] ) . length ) . toBe ( 1 ) ;
441+ } ) ;
442+
443+ it ( 'should be idempotent - disabling killpower multiple times' , async ( ) => {
444+ const config : UPSConfigInput = {
445+ killUps : UPSKillPower . NO ,
446+ service : UPSServiceState . ENABLE ,
447+ upsType : UPSType . USB ,
448+ } ;
449+
450+ // Disable killpower twice (when it's not enabled)
451+ await service . configureUPS ( config ) ;
452+ const firstContent = await readFile ( tempRc6Path , 'utf-8' ) ;
453+
454+ await service . configureUPS ( config ) ;
455+ const secondContent = await readFile ( tempRc6Path , 'utf-8' ) ;
456+
457+ // Content should be the same after second run
458+ expect ( firstContent ) . toBe ( secondContent ) ;
459+ expect ( secondContent ) . not . toContain ( 'apccontrol killpower' ) ;
460+ } ) ;
461+ } ) ;
327462} ) ;
0 commit comments