1- import { describe , expect , it } from 'vitest'
1+ import { describe , expect , it , vi , beforeEach } from 'vitest'
22
3- import { migratePlatformAliasInPluginConfigs } from './configMigration.js'
3+ // Mock node:fs before any imports that use it — vitest hoists vi.mock automatically.
4+ vi . mock ( 'node:fs' , ( ) => ( {
5+ readFileSync : vi . fn ( ) ,
6+ writeFileSync : vi . fn ( ) ,
7+ } ) )
8+
9+ import * as fs from 'node:fs'
10+ import { migratePlatformAliasInPluginConfigs , migrateLegacyPlatformAlias } from './configMigration.js'
411
512describe ( 'config migration' , ( ) => {
613 it ( 'should migrate legacy platform alias in plugin config entries' , ( ) => {
@@ -26,4 +33,117 @@ describe('config migration', () => {
2633 expect ( updated ) . toBe ( 0 )
2734 expect ( pluginConfigs [ 0 ] . platform ) . toBe ( 'HomebridgeUpdater' )
2835 } )
36+
37+ it ( 'should return 0 for a non-array input' , ( ) => {
38+ expect ( migratePlatformAliasInPluginConfigs ( null as any ) ) . toBe ( 0 )
39+ expect ( migratePlatformAliasInPluginConfigs ( undefined as any ) ) . toBe ( 0 )
40+ } )
41+ } )
42+
43+ describe ( 'migrateLegacyPlatformAlias (direct file migration)' , ( ) => {
44+ const FAKE_STORAGE = '/fake/homebridge'
45+ const FAKE_CONFIG_PATH = `${ FAKE_STORAGE } /config.json`
46+
47+ const readFileSyncMock = vi . mocked ( fs . readFileSync )
48+ const writeFileSyncMock = vi . mocked ( fs . writeFileSync )
49+
50+ function makeApi ( storagePath : string | undefined ) {
51+ return {
52+ user : { storagePath : ( ) => storagePath } ,
53+ } as any
54+ }
55+
56+ beforeEach ( ( ) => {
57+ vi . clearAllMocks ( )
58+ } )
59+
60+ it ( 'should do nothing when storagePath is missing' , async ( ) => {
61+ await migrateLegacyPlatformAlias ( makeApi ( undefined ) )
62+
63+ expect ( readFileSyncMock ) . not . toHaveBeenCalled ( )
64+ expect ( writeFileSyncMock ) . not . toHaveBeenCalled ( )
65+ } )
66+
67+ it ( 'should migrate PluginUpdate → HomebridgeUpdater entries directly in config.json' , async ( ) => {
68+ const originalConfig = {
69+ bridge : { name : 'Homebridge' , username : 'AA:BB:CC:DD:EE:FF' , port : 51826 } ,
70+ platforms : [
71+ { platform : 'PluginUpdate' , name : 'Plugin Update' , checkPluginUpdates : true } ,
72+ { platform : 'OtherPlugin' , name : 'Other' } ,
73+ ] ,
74+ }
75+
76+ readFileSyncMock . mockReturnValue ( JSON . stringify ( originalConfig ) as any )
77+ writeFileSyncMock . mockImplementation ( ( ) => { } )
78+
79+ await migrateLegacyPlatformAlias ( makeApi ( FAKE_STORAGE ) )
80+
81+ expect ( writeFileSyncMock ) . toHaveBeenCalledTimes ( 1 )
82+ const [ writtenPath , writtenData ] = writeFileSyncMock . mock . calls [ 0 ] as [ string , string , string ]
83+ expect ( writtenPath ) . toBe ( FAKE_CONFIG_PATH )
84+
85+ const written = JSON . parse ( writtenData )
86+ expect ( written . platforms [ 0 ] . platform ) . toBe ( 'HomebridgeUpdater' )
87+ expect ( written . platforms [ 0 ] . name ) . toBe ( 'Plugin Update' )
88+ expect ( written . platforms [ 0 ] . checkPluginUpdates ) . toBe ( true )
89+ // Unrelated platform entries must remain untouched
90+ expect ( written . platforms [ 1 ] . platform ) . toBe ( 'OtherPlugin' )
91+ } )
92+
93+ it ( 'should not write config.json when no legacy entries exist' , async ( ) => {
94+ const currentConfig = {
95+ bridge : { name : 'Homebridge' , username : 'AA:BB:CC:DD:EE:FF' , port : 51826 } ,
96+ platforms : [
97+ { platform : 'HomebridgeUpdater' , name : 'Updater' } ,
98+ ] ,
99+ }
100+
101+ // readFileSync is called at least once (for config.json). Any extra calls (e.g. from
102+ // the UiApi fallback path) should also not find legacy entries.
103+ readFileSyncMock . mockReturnValue ( JSON . stringify ( currentConfig ) as any )
104+ writeFileSyncMock . mockImplementation ( ( ) => { } )
105+
106+ await migrateLegacyPlatformAlias ( makeApi ( FAKE_STORAGE ) )
107+
108+ expect ( writeFileSyncMock ) . not . toHaveBeenCalled ( )
109+ } )
110+
111+ it ( 'should not throw when config.json cannot be read' , async ( ) => {
112+ readFileSyncMock . mockImplementation ( ( ) => {
113+ throw new Error ( 'ENOENT: no such file' )
114+ } )
115+ writeFileSyncMock . mockImplementation ( ( ) => { } )
116+
117+ await expect ( migrateLegacyPlatformAlias ( makeApi ( FAKE_STORAGE ) ) ) . resolves . toBeUndefined ( )
118+ expect ( writeFileSyncMock ) . not . toHaveBeenCalled ( )
119+ } )
120+
121+ it ( 'should preserve all config fields (_bridge, etc.) when migrating' , async ( ) => {
122+ const configWithBridge = {
123+ bridge : { name : 'Homebridge' , username : 'AA:BB:CC:DD:EE:FF' , port : 51826 } ,
124+ platforms : [
125+ {
126+ platform : 'PluginUpdate' ,
127+ name : 'Plugin Update' ,
128+ _bridge : { username : '11:22:33:44:55:66' , port : 51828 , name : 'Plugin Update Child Bridge' } ,
129+ checkPluginUpdates : true ,
130+ autoUpdatePlugins : false ,
131+ } ,
132+ ] ,
133+ }
134+
135+ readFileSyncMock . mockReturnValue ( JSON . stringify ( configWithBridge ) as any )
136+ writeFileSyncMock . mockImplementation ( ( ) => { } )
137+
138+ await migrateLegacyPlatformAlias ( makeApi ( FAKE_STORAGE ) )
139+
140+ expect ( writeFileSyncMock ) . toHaveBeenCalledTimes ( 1 )
141+ const written = JSON . parse ( ( writeFileSyncMock . mock . calls [ 0 ] as [ string , string , string ] ) [ 1 ] )
142+ const migratedPlatform = written . platforms [ 0 ]
143+ expect ( migratedPlatform . platform ) . toBe ( 'HomebridgeUpdater' )
144+ // Child bridge config must be preserved intact
145+ expect ( migratedPlatform . _bridge ) . toEqual ( configWithBridge . platforms [ 0 ] . _bridge )
146+ expect ( migratedPlatform . checkPluginUpdates ) . toBe ( true )
147+ expect ( migratedPlatform . autoUpdatePlugins ) . toBe ( false )
148+ } )
29149} )
0 commit comments