2023-06-21 17:52:17 +02:00
import {
2026-07-14 08:08:27 +05:30
jest ,
describe ,
it ,
expect ,
beforeEach ,
afterEach ,
afterAll
} from '@jest/globals' ;
import { fileURLToPath } from 'url' ;
import path from 'path' ;
import fs from 'fs' ;
const __dirname = path . dirname ( fileURLToPath ( import . meta . url ));
// Mock @actions modules before importing anything that depends on them
jest . unstable_mockModule ( '@actions/core' , () => ({
info : jest.fn (),
warning : jest.fn (),
debug : jest.fn (),
error : jest.fn (),
notice : jest.fn (),
setFailed : jest.fn (),
setOutput : jest.fn (),
getInput : jest.fn (),
getBooleanInput : jest.fn (),
getMultilineInput : jest.fn (),
addPath : jest.fn (),
exportVariable : jest.fn (),
saveState : jest.fn (),
getState : jest.fn (),
setSecret : jest.fn (),
isDebug : jest.fn (() => false ),
startGroup : jest.fn (),
endGroup : jest.fn (),
group : jest.fn (( _name : string , fn : () => Promise < unknown >) => fn ()),
toPlatformPath : jest.fn (( p : string ) => p ),
toWin32Path : jest.fn (( p : string ) => p ),
toPosixPath : jest.fn (( p : string ) => p )
}));
jest . unstable_mockModule ( '@actions/cache' , () => ({
saveCache : jest.fn (),
restoreCache : jest.fn (),
isFeatureAvailable : jest.fn ()
}));
jest . unstable_mockModule ( '@actions/glob' , () => ({
hashFiles : jest.fn (),
create : jest.fn ()
}));
jest . unstable_mockModule ( '@actions/exec' , () => ({
exec : jest.fn (),
getExecOutput : jest.fn ()
}));
jest . unstable_mockModule ( '@actions/io' , () => ({
which : jest.fn (),
mkdirP : jest.fn (),
rmRF : jest.fn (),
mv : jest.fn (),
cp : jest.fn ()
}));
// Dynamic imports after mocking
const core = await import ( '@actions/core' );
const cache = await import ( '@actions/cache' );
const glob = await import ( '@actions/glob' );
const exec = await import ( '@actions/exec' );
const utils = await import ( '../src/cache-utils.js' );
const cacheUtils = await import ( '../src/cache-utils.js' );
// @ts-ignore - test file outside rootDir
const { MockGlobber } = await import ( './mock/glob-mock.js' );
import type { PackageManagerInfo } from '../src/cache-utils.js' ;
const {
2023-06-21 17:52:17 +02:00
isCacheFeatureAvailable ,
supportedPackageManagers ,
2024-10-21 18:41:32 +02:00
isGhes ,
2023-06-27 13:07:43 +02:00
resetProjectDirectoriesMemoized
2026-07-14 08:08:27 +05:30
} = utils ;
// Helper: mock exec.getExecOutput to simulate getCommandOutput behavior
function mockGetCommandOutput (
spy : jest.Mock ,
fn : ( command : string , cwd? : string ) => string
) {
spy . mockImplementation ( async ( command : any , _args : any , options : any ) => ({
stdout : fn ( command , options ? . cwd ),
stderr : '' ,
exitCode : 0
}));
}
function mockGetCommandOutputOnce ( spy : jest.Mock , result : string ) {
spy . mockImplementationOnce ( async () => ({
stdout : result ,
stderr : '' ,
exitCode : 0
}));
}
2021-06-16 09:52:44 +03:00
describe ( 'cache-utils' , () => {
const versionYarn1 = '1.2.3' ;
2026-07-14 08:08:27 +05:30
let debugSpy : jest.Mock ;
let getExecOutputSpy : jest.Mock ;
let isFeatureAvailable : jest.Mock ;
let info : jest.Mock ;
let warningSpy : jest.Mock ;
let fsRealPathSyncSpy : jest.SpiedFunction < typeof fs.realpathSync >;
2021-06-16 09:52:44 +03:00
beforeEach (() => {
2023-07-13 14:06:23 +02:00
console . log ( '::stop-commands::stoptoken' );
2021-06-16 09:52:44 +03:00
process . env [ 'GITHUB_WORKSPACE' ] = path . join ( __dirname , 'data' );
2026-07-14 08:08:27 +05:30
debugSpy = core . debug as jest . Mock ;
debugSpy . mockImplementation (( _msg : any ) => {});
2022-03-31 21:10:37 +02:00
2026-07-14 08:08:27 +05:30
info = core . info as jest . Mock ;
warningSpy = core . warning as jest . Mock ;
2022-03-31 21:10:37 +02:00
2026-07-14 08:08:27 +05:30
isFeatureAvailable = cache . isFeatureAvailable as jest . Mock ;
getExecOutputSpy = exec . getExecOutput as jest . Mock ;
2023-07-13 14:06:23 +02:00
fsRealPathSyncSpy = jest . spyOn ( fs , 'realpathSync' );
2026-07-14 08:08:27 +05:30
fsRealPathSyncSpy . mockImplementation (( dirName : any ) => {
2023-07-13 14:06:23 +02:00
return dirName ;
});
2021-06-16 09:52:44 +03:00
});
2023-07-13 14:06:23 +02:00
afterEach (() => {
jest . resetAllMocks ();
jest . clearAllMocks ();
});
afterAll ( async () => {
console . log ( '::stoptoken::' );
jest . restoreAllMocks ();
}, 100000 );
2021-06-16 09:52:44 +03:00
describe ( 'getPackageManagerInfo' , () => {
2021-06-29 13:34:35 +03:00
it . each < [ string , PackageManagerInfo | null ] > ([
2021-06-16 09:52:44 +03:00
[ 'npm' , utils . supportedPackageManagers . npm ],
2021-06-30 16:44:51 +01:00
[ 'pnpm' , utils . supportedPackageManagers . pnpm ],
2023-06-21 17:52:17 +02:00
[ 'yarn' , utils . supportedPackageManagers . yarn ],
2021-06-16 09:52:44 +03:00
[ 'yarn1' , null ],
[ 'yarn2' , null ],
[ 'npm7' , null ]
])( 'getPackageManagerInfo for %s is %o' , async ( packageManager , result ) => {
2026-07-14 08:08:27 +05:30
mockGetCommandOutputOnce ( getExecOutputSpy , versionYarn1 );
2021-06-16 09:52:44 +03:00
await expect ( utils . getPackageManagerInfo ( packageManager )). resolves . toBe (
result
);
});
});
2022-03-31 21:10:37 +02:00
it ( 'isCacheFeatureAvailable for GHES is false' , () => {
isFeatureAvailable . mockImplementation (() => false );
process . env [ 'GITHUB_SERVER_URL' ] = 'https://www.test.com' ;
2022-12-09 12:05:59 +01:00
expect ( isCacheFeatureAvailable ()). toBeFalsy ();
expect ( warningSpy ). toHaveBeenCalledWith (
2022-03-31 21:10:37 +02:00
'Cache action is only supported on GHES version >= 3.5. If you are on version >=3.5 Please check with GHES admin if Actions cache service is enabled or not.'
);
});
it ( 'isCacheFeatureAvailable for GHES has an interhal error' , () => {
isFeatureAvailable . mockImplementation (() => false );
process . env [ 'GITHUB_SERVER_URL' ] = '' ;
isCacheFeatureAvailable ();
expect ( warningSpy ). toHaveBeenCalledWith (
'The runner was not able to contact the cache service. Caching will be skipped'
);
});
it ( 'isCacheFeatureAvailable for GHES is available' , () => {
isFeatureAvailable . mockImplementation (() => true );
expect ( isCacheFeatureAvailable ()). toStrictEqual ( true );
});
2021-06-16 09:52:44 +03:00
afterEach (() => {
2022-03-31 21:10:37 +02:00
process . env [ 'GITHUB_SERVER_URL' ] = '' ;
2021-06-16 09:52:44 +03:00
jest . resetAllMocks ();
jest . clearAllMocks ();
});
2023-06-21 17:52:17 +02:00
describe ( 'getCacheDirectoriesPaths' , () => {
2026-07-14 08:08:27 +05:30
let existsSpy : jest.SpiedFunction < typeof fs.existsSync >;
let lstatSpy : jest.SpiedFunction < typeof fs.lstatSync >;
let globCreateSpy : jest.Mock ;
2023-06-21 17:52:17 +02:00
beforeEach (() => {
existsSpy = jest . spyOn ( fs , 'existsSync' );
existsSpy . mockImplementation (() => true );
lstatSpy = jest . spyOn ( fs , 'lstatSync' );
2026-07-14 08:08:27 +05:30
lstatSpy . mockImplementation (
( _arg : any ) => ({ isDirectory : () => true }) as any
);
2023-06-21 17:52:17 +02:00
2026-07-14 08:08:27 +05:30
globCreateSpy = glob . create as jest . Mock ;
globCreateSpy . mockImplementation (( _pattern : any ) =>
MockGlobber . create ([ '/foo' , '/bar' ])
2023-06-21 17:52:17 +02:00
);
2023-06-27 13:07:43 +02:00
resetProjectDirectoriesMemoized ();
2023-06-21 17:52:17 +02:00
});
afterEach (() => {
existsSpy . mockRestore ();
lstatSpy . mockRestore ();
});
it . each ([
[ supportedPackageManagers . npm , '' ],
[ supportedPackageManagers . npm , '/dir/file.lock' ],
[ supportedPackageManagers . npm , '/**/file.lock' ],
[ supportedPackageManagers . pnpm , '' ],
[ supportedPackageManagers . pnpm , '/dir/file.lock' ],
[ supportedPackageManagers . pnpm , '/**/file.lock' ]
])(
'getCacheDirectoriesPaths should return one dir for non yarn' ,
async ( packageManagerInfo , cacheDependency ) => {
2026-07-14 08:08:27 +05:30
mockGetCommandOutput ( getExecOutputSpy , () => 'foo' );
2023-06-21 17:52:17 +02:00
const dirs = await cacheUtils . getCacheDirectories (
packageManagerInfo ,
cacheDependency
);
expect ( dirs ). toEqual ([ 'foo' ]);
2026-07-14 08:08:27 +05:30
expect ( getExecOutputSpy ). toHaveBeenCalledTimes ( 1 );
2023-06-21 17:52:17 +02:00
}
);
it ( 'getCacheDirectoriesPaths should return one dir for yarn without cacheDependency' , async () => {
2026-07-14 08:08:27 +05:30
mockGetCommandOutput ( getExecOutputSpy , () => 'foo' );
2023-06-21 17:52:17 +02:00
const dirs = await cacheUtils . getCacheDirectories (
supportedPackageManagers . yarn ,
''
);
expect ( dirs ). toEqual ([ 'foo' ]);
});
it . each ([
[ supportedPackageManagers . npm , '' ],
[ supportedPackageManagers . npm , '/dir/file.lock' ],
[ supportedPackageManagers . npm , '/**/file.lock' ],
[ supportedPackageManagers . pnpm , '' ],
[ supportedPackageManagers . pnpm , '/dir/file.lock' ],
[ supportedPackageManagers . pnpm , '/**/file.lock' ],
[ supportedPackageManagers . yarn , '' ],
[ supportedPackageManagers . yarn , '/dir/file.lock' ],
[ supportedPackageManagers . yarn , '/**/file.lock' ]
])(
'getCacheDirectoriesPaths should throw for getCommandOutput returning empty' ,
async ( packageManagerInfo , cacheDependency ) => {
2026-07-14 08:08:27 +05:30
mockGetCommandOutput ( getExecOutputSpy , ( command : string ) =>
2023-06-21 17:52:17 +02:00
command . includes ( 'version' ) ? '1.' : ''
);
await expect (
cacheUtils . getCacheDirectories ( packageManagerInfo , cacheDependency )
2026-07-14 08:08:27 +05:30
). rejects . toThrow ();
2023-06-21 17:52:17 +02:00
}
);
it . each ([
[ supportedPackageManagers . yarn , '/dir/file.lock' ],
[ supportedPackageManagers . yarn , '/**/file.lock' ]
])(
'getCacheDirectoriesPaths should nothrow in case of having not directories' ,
async ( packageManagerInfo , cacheDependency ) => {
2026-07-14 08:08:27 +05:30
lstatSpy . mockImplementation (
( _arg : any ) => ({ isDirectory : () => false }) as any
);
2023-06-21 17:52:17 +02:00
await cacheUtils . getCacheDirectories (
packageManagerInfo ,
cacheDependency
);
expect ( warningSpy ). toHaveBeenCalledTimes ( 1 );
expect ( warningSpy ). toHaveBeenCalledWith (
`No existing directories found containing cache-dependency-path=" ${ cacheDependency } "`
);
}
);
it . each ([ '1.1.1' , '2.2.2' ])(
'getCacheDirectoriesPaths yarn v%s should return one dir without cacheDependency' ,
async version => {
2026-07-14 08:08:27 +05:30
mockGetCommandOutputOnce ( getExecOutputSpy , version );
mockGetCommandOutputOnce ( getExecOutputSpy , `foo ${ version } ` );
2023-06-21 17:52:17 +02:00
const dirs = await cacheUtils . getCacheDirectories (
supportedPackageManagers . yarn ,
''
);
expect ( dirs ). toEqual ([ `foo ${ version } ` ]);
}
);
it . each ([ '1.1.1' , '2.2.2' ])(
'getCacheDirectoriesPaths yarn v%s should return 2 dirs with globbed cacheDependency' ,
async version => {
let dirNo = 1 ;
2026-07-14 08:08:27 +05:30
mockGetCommandOutput ( getExecOutputSpy , ( command : string ) =>
2023-06-21 17:52:17 +02:00
command . includes ( 'version' ) ? version : `file_ ${ version } _ ${ dirNo ++ } `
);
2026-07-14 08:08:27 +05:30
globCreateSpy . mockImplementation (( _pattern : any ) =>
MockGlobber . create ([ '/tmp/dir1/file' , '/tmp/dir2/file' ])
2023-06-21 17:52:17 +02:00
);
const dirs = await cacheUtils . getCacheDirectories (
supportedPackageManagers . yarn ,
'/tmp/**/file'
);
expect ( dirs ). toEqual ([ `file_ ${ version } _1` , `file_ ${ version } _2` ]);
}
);
it . each ([ '1.1.1' , '2.2.2' ])(
'getCacheDirectoriesPaths yarn v%s should return 2 dirs with globbed cacheDependency expanding to duplicates' ,
async version => {
let dirNo = 1 ;
2026-07-14 08:08:27 +05:30
mockGetCommandOutput ( getExecOutputSpy , ( command : string ) =>
2023-06-21 17:52:17 +02:00
command . includes ( 'version' ) ? version : `file_ ${ version } _ ${ dirNo ++ } `
);
2026-07-14 08:08:27 +05:30
globCreateSpy . mockImplementation (( _pattern : any ) =>
MockGlobber . create ([
'/tmp/dir1/file' ,
'/tmp/dir2/file' ,
'/tmp/dir1/file'
])
2023-06-21 17:52:17 +02:00
);
const dirs = await cacheUtils . getCacheDirectories (
supportedPackageManagers . yarn ,
'/tmp/**/file'
);
expect ( dirs ). toEqual ([ `file_ ${ version } _1` , `file_ ${ version } _2` ]);
}
);
it . each ([ '1.1.1' , '2.2.2' ])(
'getCacheDirectoriesPaths yarn v%s should return 2 uniq dirs despite duplicate cache directories' ,
async version => {
let dirNo = 1 ;
2026-07-14 08:08:27 +05:30
mockGetCommandOutput ( getExecOutputSpy , ( command : string ) =>
2023-06-21 17:52:17 +02:00
command . includes ( 'version' )
? version
: `file_ ${ version } _ ${ dirNo ++ % 2 } `
);
2026-07-14 08:08:27 +05:30
globCreateSpy . mockImplementation (( _pattern : any ) =>
MockGlobber . create ([
'/tmp/dir1/file' ,
'/tmp/dir2/file' ,
'/tmp/dir3/file'
])
2023-06-21 17:52:17 +02:00
);
const dirs = await cacheUtils . getCacheDirectories (
supportedPackageManagers . yarn ,
'/tmp/**/file'
);
expect ( dirs ). toEqual ([ `file_ ${ version } _1` , `file_ ${ version } _0` ]);
2026-07-14 08:08:27 +05:30
expect ( getExecOutputSpy ). toHaveBeenCalledTimes ( 6 );
expect ( getExecOutputSpy ). toHaveBeenCalledWith (
2023-06-21 17:52:17 +02:00
'yarn --version' ,
2026-07-14 08:08:27 +05:30
undefined ,
expect . objectContaining ({ cwd : '/tmp/dir1' })
2023-06-21 17:52:17 +02:00
);
2026-07-14 08:08:27 +05:30
expect ( getExecOutputSpy ). toHaveBeenCalledWith (
2023-06-21 17:52:17 +02:00
'yarn --version' ,
2026-07-14 08:08:27 +05:30
undefined ,
expect . objectContaining ({ cwd : '/tmp/dir2' })
2023-06-21 17:52:17 +02:00
);
2026-07-14 08:08:27 +05:30
expect ( getExecOutputSpy ). toHaveBeenCalledWith (
2023-06-21 17:52:17 +02:00
'yarn --version' ,
2026-07-14 08:08:27 +05:30
undefined ,
expect . objectContaining ({ cwd : '/tmp/dir3' })
2023-06-21 17:52:17 +02:00
);
2026-07-14 08:08:27 +05:30
expect ( getExecOutputSpy ). toHaveBeenCalledWith (
2023-06-21 17:52:17 +02:00
version . startsWith ( '1.' )
? 'yarn cache dir'
: 'yarn config get cacheFolder' ,
2026-07-14 08:08:27 +05:30
undefined ,
expect . objectContaining ({ cwd : '/tmp/dir1' })
2023-06-21 17:52:17 +02:00
);
2026-07-14 08:08:27 +05:30
expect ( getExecOutputSpy ). toHaveBeenCalledWith (
2023-06-21 17:52:17 +02:00
version . startsWith ( '1.' )
? 'yarn cache dir'
: 'yarn config get cacheFolder' ,
2026-07-14 08:08:27 +05:30
undefined ,
expect . objectContaining ({ cwd : '/tmp/dir2' })
2023-06-21 17:52:17 +02:00
);
2026-07-14 08:08:27 +05:30
expect ( getExecOutputSpy ). toHaveBeenCalledWith (
2023-06-21 17:52:17 +02:00
version . startsWith ( '1.' )
? 'yarn cache dir'
: 'yarn config get cacheFolder' ,
2026-07-14 08:08:27 +05:30
undefined ,
expect . objectContaining ({ cwd : '/tmp/dir3' })
2023-06-21 17:52:17 +02:00
);
}
);
it . each ([ '1.1.1' , '2.2.2' ])(
'getCacheDirectoriesPaths yarn v%s should return 4 dirs with multiple globs' ,
async version => {
const cacheDependencyPath = `/tmp/dir1/file
/tmp/dir2/file
/tmp/**/file
` ;
2026-07-14 08:08:27 +05:30
globCreateSpy . mockImplementation (( _pattern : any ) =>
MockGlobber . create ([
'/tmp/dir1/file' ,
'/tmp/dir2/file' ,
'/tmp/dir3/file' ,
'/tmp/dir4/file'
])
2023-06-21 17:52:17 +02:00
);
let dirNo = 1 ;
2026-07-14 08:08:27 +05:30
mockGetCommandOutput ( getExecOutputSpy , ( command : string ) =>
2023-06-21 17:52:17 +02:00
command . includes ( 'version' ) ? version : `file_ ${ version } _ ${ dirNo ++ } `
);
const dirs = await cacheUtils . getCacheDirectories (
supportedPackageManagers . yarn ,
cacheDependencyPath
);
expect ( dirs ). toEqual ([
`file_ ${ version } _1` ,
`file_ ${ version } _2` ,
`file_ ${ version } _3` ,
`file_ ${ version } _4`
]);
}
);
});
2021-06-16 09:52:44 +03:00
});
2024-10-21 18:41:32 +02:00
describe ( 'isGhes' , () => {
const pristineEnv = process . env ;
beforeEach (() => {
jest . resetModules ();
process . env = {... pristineEnv };
});
afterAll (() => {
process . env = pristineEnv ;
});
it ( 'returns false when the GITHUB_SERVER_URL environment variable is not defined' , () => {
delete process . env [ 'GITHUB_SERVER_URL' ];
expect ( isGhes ()). toBeFalsy ();
});
it ( 'returns false when the GITHUB_SERVER_URL environment variable is set to github.com' , () => {
process . env [ 'GITHUB_SERVER_URL' ] = 'https://github.com' ;
expect ( isGhes ()). toBeFalsy ();
});
it ( 'returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL' , () => {
process . env [ 'GITHUB_SERVER_URL' ] = 'https://contoso.ghe.com' ;
expect ( isGhes ()). toBeFalsy ();
});
it ( 'returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix' , () => {
process . env [ 'GITHUB_SERVER_URL' ] = 'https://mock-github.localhost' ;
expect ( isGhes ()). toBeFalsy ();
});
it ( 'returns true when the GITHUB_SERVER_URL environment variable is set to some other URL' , () => {
process . env [ 'GITHUB_SERVER_URL' ] = 'https://src.onpremise.fabrikam.com' ;
expect ( isGhes ()). toBeTruthy ();
});
});