1- import { mkdirSync , readdir , rmdirSync , symlinkSync , writeFileSync } from 'node:fs' ;
1+ import { existsSync , mkdirSync , readdir , rmSync , symlinkSync , writeFileSync } from 'node:fs' ;
22import { globSync } from 'tinyglobby' ;
33import { afterAll , afterEach , beforeEach , describe , expect , test , vi } from 'vitest' ;
44
55import { copyfiles , createDir } from '../index' ;
66
7+ let shouldMockReadError = false ;
8+ const error = new Error ( 'Mock read error' ) ;
9+
10+ vi . mock ( 'node:fs' , async ( ) => {
11+ const actual = await vi . importActual < typeof import ( 'node:fs' ) > ( 'node:fs' ) ;
12+ return {
13+ ...actual ,
14+ createReadStream : ( ...args : any [ ] ) => {
15+ if ( shouldMockReadError ) {
16+ const { Readable } = require ( 'node:stream' ) ;
17+ const stream = new Readable ( { read ( ) { } } ) ;
18+ setImmediate ( ( ) => stream . emit ( 'error' , error ) ) ;
19+ return stream ;
20+ }
21+ // fallback to real implementation
22+ return ( actual . createReadStream as any ) ( ...args ) ;
23+ }
24+ } ;
25+ } ) ;
26+
727async function cleanupFolders ( ) {
828 try {
9- rmdirSync ( 'input' , { recursive : true } ) ;
10- rmdirSync ( 'output' , { recursive : true } ) ;
29+ rmSync ( 'input' , { recursive : true , force : true } ) ;
30+ rmSync ( 'output' , { recursive : true , force : true } ) ;
1131 } catch ( e ) { }
1232}
1333
1434describe ( 'copyfiles' , ( ) => {
1535 afterEach ( async ( ) => {
16- cleanupFolders ( ) ;
36+ await cleanupFolders ( ) ;
1737 } ) ;
1838
19- afterAll ( ( ) => cleanupFolders ( ) ) ;
39+ afterAll ( async ( ) => {
40+ await cleanupFolders ( ) ;
41+ } ) ;
2042
2143 beforeEach ( ( ) => {
2244 createDir ( 'input/other' ) ;
@@ -43,8 +65,6 @@ describe('copyfiles', () => {
4365 copyfiles ( [ 'input/*.txt' , 'output' ] , { } , ( err ) => {
4466 console . error ( err , 'copyfiles' ) ;
4567 readdir ( 'output/input' , async ( err , files ) => {
46- // console.error(err, 'readdir');
47- // 'correct number of things'
4868 expect ( files ) . toEqual ( [ 'a.txt' , 'b.txt' ] ) ;
4969 done ( ) ;
5070 } ) ;
@@ -184,7 +204,9 @@ describe('copyfiles', () => {
184204 copyfiles ( [ 'input/**/*.txt' , 'output' ] , { flat : true , verbose : true } , ( err ) => {
185205 readdir ( 'output' , ( err , files ) => {
186206 expect ( files ) . toEqual ( [ 'a.txt' , 'b.txt' ] ) ;
187- expect ( logSpy ) . toHaveBeenCalledWith ( 'glob found' , [ 'input/b.txt' , 'input/other/a.txt' ] ) ;
207+ const globCall = logSpy . mock . calls . find ( call => call [ 0 ] === 'glob found' ) ;
208+ expect ( globCall ) . toBeTruthy ( ) ;
209+ expect ( new Set ( globCall ! [ 1 ] ) ) . toEqual ( new Set ( [ 'input/b.txt' , 'input/other/a.txt' ] ) ) ;
188210 expect ( logSpy ) . toHaveBeenCalledWith ( 'copy:' , { from : 'input/other/a.txt' , to : 'output/a.txt' } ) ;
189211 expect ( logSpy ) . toHaveBeenCalledWith ( 'copy:' , { from : 'input/b.txt' , to : 'output/b.txt' } ) ;
190212 expect ( logSpy ) . toHaveBeenCalledWith ( 'Files copied: 2' ) ;
@@ -193,6 +215,88 @@ describe('copyfiles', () => {
193215 } ) ;
194216 } ) ) ;
195217
218+ test ( 'createDir does not throw if dir exists' , ( ) => {
219+ createDir ( 'input' ) ;
220+ expect ( ( ) => createDir ( 'input' ) ) . not . toThrow ( ) ;
221+ } ) ;
222+
223+ test ( 'throws when inFile or outDir are missing (no callback)' , ( ) => {
224+ expect ( ( ) => copyfiles ( [ 'input/**/*.txt' ] , { } ) ) . toThrow (
225+ 'Please make sure to provide both <inFile> and <outDirectory>, i.e.: "copyfiles <inFile> <outDirectory>"'
226+ ) ;
227+ } ) ;
228+
229+ test ( 'callback called when no files to copy' , ( ) => new Promise ( ( done : any ) => {
230+ copyfiles ( [ 'input/doesnotexist/*.txt' , 'output' ] , { } , ( err ) => {
231+ expect ( err ) . toBeUndefined ( ) ;
232+ done ( ) ;
233+ } ) ;
234+ } ) ) ;
235+
236+ test ( 'copyFileStream handles read error' , ( ) => new Promise ( ( done : any ) => {
237+ writeFileSync ( 'input/bad.txt' , 'bad' ) ; // <-- Ensure the file exists!
238+ shouldMockReadError = true ;
239+ copyfiles ( [ 'input/bad.txt' , 'output' ] , { } , ( err ) => {
240+ expect ( err ) . toBeInstanceOf ( Error ) ;
241+ expect ( err ?. message ) . toBe ( 'Mock read error' ) ;
242+ shouldMockReadError = false ;
243+ done ( ) ;
244+ } ) ;
245+ } ) ) ;
246+
247+ test ( 'throws when flat & up used together' , ( ) => {
248+ expect ( ( ) => copyfiles ( [ 'input/**/*.txt' , 'output' ] , { flat : true , up : 1 } ) ) . toThrow (
249+ 'Cannot use --flat in conjunction with --up option.'
250+ ) ;
251+ } ) ;
252+
253+ test ( 'calls callback with error when nothing copied and options.error is set' , ( ) => new Promise ( ( done : any ) => {
254+ copyfiles ( [ 'input/doesnotexist/*.txt' , 'output' ] , { error : true } , ( err ) => {
255+ expect ( err ) . toBeInstanceOf ( Error ) ;
256+ expect ( err ?. message ) . toBe ( 'nothing copied' ) ;
257+ done ( ) ;
258+ } ) ;
259+ } ) ) ;
260+
261+ test ( 'logs and calls callback when nothing copied and verbose/stat is set' , ( ) => new Promise ( ( done : any ) => {
262+ const logSpy = vi . spyOn ( global . console , 'log' ) . mockReturnValue ( ) ;
263+ const timeSpy = vi . spyOn ( global . console , 'timeEnd' ) . mockReturnValue ( ) ;
264+ copyfiles ( [ 'input/doesnotexist/*.txt' , 'output' ] , { verbose : true } , ( err ) => {
265+ expect ( logSpy ) . toHaveBeenCalledWith ( 'Files copied: 0' ) ;
266+ expect ( timeSpy ) . toHaveBeenCalled ( ) ;
267+ expect ( err ) . toBeUndefined ( ) ;
268+ logSpy . mockRestore ( ) ;
269+ timeSpy . mockRestore ( ) ;
270+ done ( ) ;
271+ } ) ;
272+ } ) ) ;
273+
274+ test ( 'throws when flat & up used together (with callback)' , ( ) => new Promise ( ( done : any ) => {
275+ copyfiles ( [ 'input/**/*.txt' , 'output' ] , { flat : true , up : 1 } , ( err ) => {
276+ expect ( err ) . toBeInstanceOf ( Error ) ;
277+ expect ( err ?. message ) . toBe ( 'Cannot use --flat in conjunction with --up option.' ) ;
278+ done ( ) ;
279+ } ) ;
280+ } ) ) ;
281+
282+ test ( 'throws when nothing copied and options.error is set (no callback)' , ( ) => {
283+ expect ( ( ) => {
284+ copyfiles ( [ 'input/doesnotexist/*.txt' , 'output' ] , { error : true } ) ;
285+ } ) . toThrow ( 'nothing copied' ) ;
286+ } ) ;
287+
288+ test ( 'sets followSymbolicLinks when options.follow is true' , ( ) => new Promise ( ( done : any ) => {
289+ if ( process . platform === 'win32' ) return done ( ) ; // skip on Windows (symlink perms)
290+ mkdirSync ( 'input/real' , { recursive : true } ) ;
291+ writeFileSync ( 'input/real/a.txt' , 'test' ) ;
292+ symlinkSync ( 'real' , 'input/link' ) ;
293+ copyfiles ( [ 'input/link/*.txt' , 'output' ] , { follow : true } , ( err ) => {
294+ expect ( err ) . toBeUndefined ( ) ;
295+ expect ( existsSync ( 'output/link/a.txt' ) ) . toBe ( true ) ;
296+ done ( ) ;
297+ } ) ;
298+ } ) ) ;
299+
196300 test ( 'verbose up' , ( ) => new Promise ( ( done : any ) => {
197301 const logSpy = vi . spyOn ( global . console , 'log' ) . mockReturnValue ( ) ;
198302 writeFileSync ( 'input/other/a.txt' , 'a' ) ;
0 commit comments