11import { dialog } from 'electron' ;
22import configStore , { type AppStore } from '../config' ;
33import eventBus from '../event-bus' ;
4+ import * as validateRootFolderChangeModule from './validate-root-folder-change' ;
45import { chooseSyncRootWithDialog , getRootVirtualDrive } from './service' ;
56
67vi . mock ( 'electron' , ( ) => ( {
@@ -36,10 +37,17 @@ vi.mock('../../shared/fs/ensure-folder-exists', () => ({
3637 ensureFolderExists : vi . fn ( ) ,
3738} ) ) ;
3839
40+ vi . mock ( './validate-root-folder-change' , ( ) => ( {
41+ validateRootFolderChange : vi . fn ( ) ,
42+ isPermissionError : vi . fn ( ) ,
43+ } ) ) ;
44+
3945describe ( 'service' , ( ) => {
4046 const configGetMock = vi . mocked ( configStore . get ) ;
4147 const configSetMock = vi . mocked ( configStore . set ) ;
4248 const eventBusEmitMock = vi . mocked ( eventBus . emit ) ;
49+ const validateRootFolderChangeMock = vi . mocked ( validateRootFolderChangeModule . validateRootFolderChange ) ;
50+ const isPermissionErrorMock = vi . mocked ( validateRootFolderChangeModule . isPermissionError ) ;
4351
4452 beforeEach ( ( ) => {
4553 const state = new Map < keyof AppStore , AppStore [ keyof AppStore ] > ( [
@@ -51,6 +59,9 @@ describe('service', () => {
5159 configSetMock . mockImplementation ( ( key , value ) => {
5260 state . set ( key , value ) ;
5361 } ) ;
62+
63+ validateRootFolderChangeMock . mockResolvedValue ( null ) ;
64+ isPermissionErrorMock . mockReturnValue ( false ) ;
5465 } ) ;
5566
5667 it ( 'should fallback to default root folder when no saved path exists' , ( ) => {
@@ -90,10 +101,91 @@ describe('service', () => {
90101
91102 const selectedPath = await chooseSyncRootWithDialog ( ) ;
92103
93- expect ( selectedPath ) . toBe ( ' /new/root/Internxt Drive/') ;
104+ expect ( selectedPath ) . toStrictEqual ( { status : 'success' , path : ' /new/root/Internxt Drive/' } ) ;
94105 expect ( eventBusEmitMock ) . toHaveBeenCalledWith ( 'SYNC_ROOT_CHANGED' , {
95106 oldPath : '/old/root/Internxt Drive/' ,
96107 newPath : '/new/root/Internxt Drive/' ,
97108 } ) ;
98109 } ) ;
110+
111+ it ( 'should return cancelled result when user dismisses folder picker' , async ( ) => {
112+ vi . mocked ( dialog . showOpenDialog ) . mockResolvedValue ( {
113+ canceled : true ,
114+ filePaths : [ ] ,
115+ } as Awaited < ReturnType < typeof dialog . showOpenDialog > > ) ;
116+
117+ const selectedPath = await chooseSyncRootWithDialog ( ) ;
118+
119+ expect ( selectedPath ) . toStrictEqual ( { status : 'cancelled' } ) ;
120+ expect ( validateRootFolderChangeMock ) . not . toHaveBeenCalled ( ) ;
121+ expect ( eventBusEmitMock ) . not . toHaveBeenCalled ( ) ;
122+ } ) ;
123+
124+ it ( 'should return validation error from validateRootFolderChange' , async ( ) => {
125+ vi . mocked ( dialog . showOpenDialog ) . mockResolvedValue ( {
126+ canceled : false ,
127+ filePaths : [ '/media/user/usb' ] ,
128+ } as Awaited < ReturnType < typeof dialog . showOpenDialog > > ) ;
129+ validateRootFolderChangeMock . mockResolvedValueOnce ( {
130+ status : 'error' ,
131+ code : 'REMOVABLE_DEVICE' ,
132+ } ) ;
133+
134+ const selectedPath = await chooseSyncRootWithDialog ( ) ;
135+
136+ expect ( selectedPath ) . toStrictEqual ( { status : 'error' , code : 'REMOVABLE_DEVICE' } ) ;
137+ expect ( eventBusEmitMock ) . not . toHaveBeenCalled ( ) ;
138+ } ) ;
139+
140+ it ( 'should not emit SYNC_ROOT_CHANGED when resulting mount path does not change' , async ( ) => {
141+ const state = new Map < keyof AppStore , AppStore [ keyof AppStore ] > ( [
142+ [ 'virtualDriveRoot' , '/old/root/' ] ,
143+ [ 'lastSavedListing' , '' ] ,
144+ ] ) ;
145+
146+ configGetMock . mockImplementation ( ( key ) => state . get ( key ) as AppStore [ typeof key ] ) ;
147+ configSetMock . mockImplementation ( ( key , value ) => {
148+ state . set ( key , value ) ;
149+ } ) ;
150+
151+ vi . mocked ( dialog . showOpenDialog ) . mockResolvedValue ( {
152+ canceled : false ,
153+ filePaths : [ '/old/root' ] ,
154+ } as Awaited < ReturnType < typeof dialog . showOpenDialog > > ) ;
155+
156+ const selectedPath = await chooseSyncRootWithDialog ( ) ;
157+
158+ expect ( selectedPath ) . toStrictEqual ( { status : 'success' , path : '/old/root/Internxt Drive/' } ) ;
159+ expect ( eventBusEmitMock ) . not . toHaveBeenCalled ( ) ;
160+ } ) ;
161+
162+ it ( 'should return INSUFFICIENT_PERMISSION when an error is caught and identified as permission error' , async ( ) => {
163+ vi . mocked ( dialog . showOpenDialog ) . mockResolvedValue ( {
164+ canceled : false ,
165+ filePaths : [ '/restricted/path' ] ,
166+ } as Awaited < ReturnType < typeof dialog . showOpenDialog > > ) ;
167+
168+ validateRootFolderChangeMock . mockRejectedValueOnce ( new Error ( 'permission denied' ) ) ;
169+ isPermissionErrorMock . mockReturnValueOnce ( true ) ;
170+
171+ const selectedPath = await chooseSyncRootWithDialog ( ) ;
172+
173+ expect ( selectedPath ) . toStrictEqual ( { status : 'error' , code : 'INSUFFICIENT_PERMISSION' } ) ;
174+ expect ( eventBusEmitMock ) . not . toHaveBeenCalled ( ) ;
175+ } ) ;
176+
177+ it ( 'should return UNKNOWN when an unknown error is caught' , async ( ) => {
178+ vi . mocked ( dialog . showOpenDialog ) . mockResolvedValue ( {
179+ canceled : false ,
180+ filePaths : [ '/problematic/path' ] ,
181+ } as Awaited < ReturnType < typeof dialog . showOpenDialog > > ) ;
182+
183+ validateRootFolderChangeMock . mockRejectedValueOnce ( new Error ( 'unexpected failure' ) ) ;
184+ isPermissionErrorMock . mockReturnValueOnce ( false ) ;
185+
186+ const selectedPath = await chooseSyncRootWithDialog ( ) ;
187+
188+ expect ( selectedPath ) . toStrictEqual ( { status : 'error' , code : 'UNKNOWN' } ) ;
189+ expect ( eventBusEmitMock ) . not . toHaveBeenCalled ( ) ;
190+ } ) ;
99191} ) ;
0 commit comments