2026-10-05 15:05:38 +02:00
import {
jest ,
describe ,
it ,
expect ,
beforeEach ,
afterEach ,
afterAll
} from '@jest/globals' ;
import fs from 'fs' ;
import path from 'path' ;
2026-07-16 02:04:36 +05:30
jest . unstable_mockModule ( '@actions/exec' , () => ({
exec : jest.fn (),
getExecOutput : jest.fn ()
}));
jest . unstable_mockModule ( '@actions/cache' , () => ({
saveCache : jest.fn (),
restoreCache : jest.fn (),
isFeatureAvailable : jest.fn ()
}));
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 )
}));
const exec = await import ( '@actions/exec' );
const cache = await import ( '@actions/cache' );
const core = await import ( '@actions/core' );
const cacheUtils = await import ( '../src/cache-utils.js' );
import type { PackageManagerInfo } from '../src/package-managers.js' ;
2022-05-25 12:07:29 +02:00
describe ( 'getCommandOutput' , () => {
//Arrange
2026-07-16 02:04:36 +05:30
const getExecOutputSpy = exec . getExecOutput as jest . Mock <
typeof exec . getExecOutput
> ;
2022-05-25 12:07:29 +02:00
it ( 'should return trimmed stdout in case of successful exit code' , async () => {
//Arrange
const stdoutResult = ' stdout ' ;
const trimmedStdout = stdoutResult . trim ();
2026-07-16 02:04:36 +05:30
getExecOutputSpy . mockImplementation ( async ( commandLine : string ) => {
return { exitCode : 0 , stdout : stdoutResult , stderr : '' };
2022-05-25 12:07:29 +02:00
});
//Act + Assert
return cacheUtils
. getCommandOutput ( 'command' )
. then ( data => expect ( data ). toBe ( trimmedStdout ));
});
it ( 'should return error in case of unsuccessful exit code' , async () => {
//Arrange
const stderrResult = 'error message' ;
2026-07-16 02:04:36 +05:30
getExecOutputSpy . mockImplementation ( async ( commandLine : string ) => {
return { exitCode : 10 , stdout : '' , stderr : stderrResult };
2022-05-25 12:07:29 +02:00
});
//Act + Assert
2023-03-08 10:45:16 +02:00
await expect ( async () => {
2022-05-25 12:07:29 +02:00
await cacheUtils . getCommandOutput ( 'command' );
}). rejects . toThrow ();
});
});
describe ( 'getPackageManagerInfo' , () => {
it ( 'should return package manager info in case of valid package manager name' , async () => {
//Arrange
const packageManagerName = 'default' ;
const expectedResult = {
2026-01-26 22:29:03 +05:30
dependencyFilePattern : 'go.mod' ,
2026-10-05 15:05:38 +02:00
cacheFolderCommandList : [
{ kind : 'modules' , command : 'go env GOMODCACHE' },
{ kind : 'build' , command : 'go env GOCACHE' }
]
2022-05-25 12:07:29 +02:00
};
//Act + Assert
return cacheUtils
. getPackageManagerInfo ( packageManagerName )
. then ( data => expect ( data ). toEqual ( expectedResult ));
});
it ( 'should throw the error in case of invalid package manager name' , async () => {
//Arrange
const packageManagerName = 'invalidName' ;
//Act + Assert
2023-03-08 10:45:16 +02:00
await expect ( async () => {
2022-05-25 12:07:29 +02:00
await cacheUtils . getPackageManagerInfo ( packageManagerName );
}). rejects . toThrow ();
});
});
describe ( 'getCacheDirectoryPath' , () => {
//Arrange
2026-07-16 02:04:36 +05:30
const getExecOutputSpy = exec . getExecOutput as jest . Mock <
typeof exec . getExecOutput
> ;
2022-05-25 12:07:29 +02:00
const validPackageManager : PackageManagerInfo = {
2026-01-26 22:29:03 +05:30
dependencyFilePattern : 'go.mod' ,
2026-10-05 15:05:38 +02:00
cacheFolderCommandList : [
{ kind : 'modules' , command : 'go env GOMODCACHE' },
{ kind : 'build' , command : 'go env GOCACHE' }
]
2022-05-25 12:07:29 +02:00
};
it ( 'should return path to the cache folders which specified package manager uses' , async () => {
//Arrange
2026-07-16 02:04:36 +05:30
getExecOutputSpy . mockImplementation ( async ( commandLine : string ) => {
2026-10-05 15:05:38 +02:00
return {
exitCode : 0 ,
stdout :
commandLine === 'go env GOMODCACHE'
? 'path/to/modules'
: 'path/to/build' ,
stderr : ''
};
2022-05-25 12:07:29 +02:00
});
2026-10-05 15:05:38 +02:00
const expectedResult = [
{ kind : 'modules' , path : 'path/to/modules' },
{ kind : 'build' , path : 'path/to/build' }
];
2022-05-25 12:07:29 +02:00
//Act + Assert
return cacheUtils
. getCacheDirectoryPath ( validPackageManager )
. then ( data => expect ( data ). toEqual ( expectedResult ));
});
2026-10-05 15:05:38 +02:00
it . each ( validPackageManager . cacheFolderCommandList )(
'preserves the $kind identity when the other command returns an empty path' ,
async ({ kind , command }) => {
getExecOutputSpy . mockImplementation ( async commandLine => ({
exitCode : 0 ,
stdout : commandLine === command ? 'path/to/cache/folder' : '' ,
stderr : ''
}));
2022-12-19 11:22:17 +01:00
2026-10-05 15:05:38 +02:00
await expect (
cacheUtils . getCacheDirectoryPath ( validPackageManager )
). resolves . toEqual ([{ kind , path : 'path/to/cache/folder' }]);
}
);
2022-12-19 11:22:17 +01:00
2026-10-05 15:05:38 +02:00
it . each ( validPackageManager . cacheFolderCommandList )(
'preserves the $kind identity when the other command fails' ,
async ({ kind , command }) => {
getExecOutputSpy . mockImplementation ( async commandLine => {
if ( commandLine !== command ) {
throw new Error ( 'Could not run go env' );
}
return { exitCode : 0 , stdout : 'path/to/cache/folder' , stderr : '' };
});
2022-12-19 11:22:17 +01:00
2026-10-05 15:05:38 +02:00
await expect (
cacheUtils . getCacheDirectoryPath ( validPackageManager )
). resolves . toEqual ([{ kind , path : 'path/to/cache/folder' }]);
expect ( core . info ). toHaveBeenCalledWith (
expect . stringContaining ( 'Could not run go env' )
);
}
);
2022-12-19 11:22:17 +01:00
it ( 'should throw if the both commands return empty str' , async () => {
2026-07-16 02:04:36 +05:30
getExecOutputSpy . mockImplementation ( async ( commandLine : string ) => {
2026-10-05 15:05:38 +02:00
return { exitCode : 0 , stdout : '' , stderr : '' };
2022-12-19 11:22:17 +01:00
});
//Act + Assert
2023-03-08 10:45:16 +02:00
await expect ( async () => {
2022-12-19 11:22:17 +01:00
await cacheUtils . getCacheDirectoryPath ( validPackageManager );
}). rejects . toThrow ();
});
2026-10-05 15:05:38 +02:00
it ( 'should throw if both commands fail' , async () => {
2026-07-16 02:04:36 +05:30
getExecOutputSpy . mockImplementation ( async ( commandLine : string ) => {
return { exitCode : 10 , stdout : '' , stderr : 'Error message' };
2022-05-25 12:07:29 +02:00
});
//Act + Assert
2023-03-08 10:45:16 +02:00
await expect ( async () => {
2022-05-25 12:07:29 +02:00
await cacheUtils . getCacheDirectoryPath ( validPackageManager );
}). rejects . toThrow ();
});
});
2026-10-05 15:05:38 +02:00
describe ( 'canRestoreCachesInParallel' , () => {
const originalPlatform = Object . getOwnPropertyDescriptor (
process ,
'platform'
) ! ;
const directoryStat = fs . lstatSync ( process . cwd ());
beforeEach (() => {
Object . defineProperty ( process , 'platform' , { value : 'linux' });
jest . spyOn ( fs , 'lstatSync' ). mockReturnValue ( directoryStat );
jest
. spyOn ( fs , 'realpathSync' )
. mockImplementation ( directory => path . resolve ( directory . toString ()));
});
afterEach (() => {
Object . defineProperty ( process , 'platform' , originalPlatform );
jest . restoreAllMocks ();
});
it . each ([
{ modules : 'cache/modules' , build : 'cache/build' , parallel : true },
{ modules : 'cache' , build : 'cache-build' , parallel : true },
{ modules : 'cache' , build : 'cache' , parallel : false },
{ modules : 'cache' , build : 'cache/build' , parallel : false },
{ modules : 'cache/modules' , build : 'cache' , parallel : false },
{ modules : 'cache' , build : 'cache/../cache' , parallel : false }
])(
'checks overlapping paths: $modules and $build' ,
({ modules , build , parallel }) => {
expect (
cacheUtils . canRestoreCachesInParallel ([
{ kind : 'modules' , path : modules },
{ kind : 'build' , path : build }
])
). toBe ( parallel );
}
);
it ( 'allows concurrent Windows restores with disjoint paths' , () => {
Object . defineProperty ( process , 'platform' , { value : 'win32' });
expect (
cacheUtils . canRestoreCachesInParallel ([
{ kind : 'modules' , path : 'cache/modules' },
{ kind : 'build' , path : 'cache/build' }
])
). toBe ( true );
});
it . each ([ 'darwin' , 'win32' ])(
'handles case-insensitive paths conservatively on %s' ,
platform => {
Object . defineProperty ( process , 'platform' , { value : platform });
expect (
cacheUtils . canRestoreCachesInParallel ([
{ kind : 'modules' , path : 'cache' },
{ kind : 'build' , path : 'CACHE/build' }
])
). toBe ( false );
}
);
it . each ([ true , false ])(
'serializes symlink aliases with missing cache directories (overlap=%s)' ,
overlap => {
const alias = path . resolve ( 'alias' );
const target = path . resolve ( 'target' );
jest . spyOn ( fs , 'lstatSync' ). mockImplementation ( directory => {
if ( directory !== alias && directory !== target ) {
throw Object . assign ( new Error ( 'Missing path' ), { code : 'ENOENT' });
}
return directoryStat ;
});
jest . spyOn ( fs , 'realpathSync' ). mockReturnValue ( target );
expect (
cacheUtils . canRestoreCachesInParallel ([
{ kind : 'modules' , path : path.join ( alias , 'modules' )},
{
kind : 'build' ,
path : path.join ( target , overlap ? 'modules/build' : 'build' )
}
])
). toBe ( false );
}
);
it . each ([ 'nested alias' , 'alias through module cache' ])(
'serializes restores when the %s resolves outside the module cache' ,
scenario => {
const modules = path . resolve ( 'modules' );
const target = path . resolve ( 'external-build' );
const alias = path . join ( modules , 'build-link' );
const build =
scenario === 'nested alias'
? path . join ( alias , 'cache' )
: path . resolve ( 'build-alias' , 'cache' );
jest . spyOn ( fs , 'realpathSync' ). mockImplementation ( directory => {
const absolute = path . resolve ( directory . toString ());
if ( absolute === build ) {
return path . join ( target , 'cache' );
}
if ( absolute === path . dirname ( build )) {
return scenario === 'nested alias' ? target : alias ;
}
return absolute ;
});
expect (
cacheUtils . canRestoreCachesInParallel ([
{ kind : 'modules' , path : modules },
{ kind : 'build' , path : build }
])
). toBe ( false );
}
);
it ( 'falls back to sequential restores when path resolution fails' , () => {
jest . spyOn ( fs , 'realpathSync' ). mockImplementation (() => {
throw new Error ( 'Cannot resolve path' );
});
expect (
cacheUtils . canRestoreCachesInParallel ([
{ kind : 'modules' , path : 'cache/modules' },
{ kind : 'build' , path : 'cache/build' }
])
). toBe ( false );
});
});
2022-05-25 12:07:29 +02:00
describe ( 'isCacheFeatureAvailable' , () => {
//Arrange
2026-07-16 02:04:36 +05:30
const isFeatureAvailableSpy = cache . isFeatureAvailable as jest . Mock <
typeof cache . isFeatureAvailable
> ;
const warningSpy = core . warning as jest . Mock < typeof core.warning >;
2022-05-25 12:07:29 +02:00
it ( 'should return true when cache feature is available' , () => {
//Arrange
isFeatureAvailableSpy . mockImplementation (() => {
return true ;
});
//Act
2023-03-08 10:45:16 +02:00
const functionResult = cacheUtils . isCacheFeatureAvailable ();
2022-05-25 12:07:29 +02:00
//Assert
expect ( functionResult ). toBeTruthy ();
});
2023-03-08 10:45:16 +02:00
it ( 'should warn when cache feature is unavailable and GHES is not used' , () => {
2022-05-25 12:07:29 +02:00
//Arrange
isFeatureAvailableSpy . mockImplementation (() => {
return false ;
});
process . env [ 'GITHUB_SERVER_URL' ] = 'https://github.com' ;
2023-03-08 10:45:16 +02:00
const warningMessage =
2022-05-25 12:07:29 +02:00
'The runner was not able to contact the cache service. Caching will be skipped' ;
//Act
cacheUtils . isCacheFeatureAvailable ();
//Assert
expect ( warningSpy ). toHaveBeenCalledWith ( warningMessage );
});
it ( 'should return false when cache feature is unavailable' , () => {
//Arrange
isFeatureAvailableSpy . mockImplementation (() => {
return false ;
});
process . env [ 'GITHUB_SERVER_URL' ] = 'https://github.com' ;
//Act
2023-03-08 10:45:16 +02:00
const functionResult = cacheUtils . isCacheFeatureAvailable ();
2022-05-25 12:07:29 +02:00
//Assert
expect ( functionResult ). toBeFalsy ();
});
2022-12-16 23:05:54 +09:00
it ( 'should warn when cache feature is unavailable and GHES is used' , () => {
2022-05-25 12:07:29 +02:00
//Arrange
isFeatureAvailableSpy . mockImplementation (() => {
return false ;
});
process . env [ 'GITHUB_SERVER_URL' ] = 'https://nongithub.com' ;
2023-03-08 10:45:16 +02:00
const warningMessage =
2022-05-25 12:07:29 +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.' ;
//Act + Assert
2022-12-16 23:05:54 +09:00
expect ( cacheUtils . isCacheFeatureAvailable ()). toBeFalsy ();
expect ( warningSpy ). toHaveBeenCalledWith ( warningMessage );
2022-05-25 12:07:29 +02:00
});
});
2024-10-21 18:56:08 +02:00
describe ( 'isGhes' , () => {
2026-07-16 02:04:36 +05:30
const pristineEnv = {... process . env };
2024-10-21 18:56:08 +02:00
beforeEach (() => {
process . env = {... pristineEnv };
});
afterAll (() => {
process . env = pristineEnv ;
});
it ( 'returns false when the GITHUB_SERVER_URL environment variable is not defined' , async () => {
delete process . env [ 'GITHUB_SERVER_URL' ];
expect ( cacheUtils . isGhes ()). toBeFalsy ();
});
it ( 'returns false when the GITHUB_SERVER_URL environment variable is set to github.com' , async () => {
process . env [ 'GITHUB_SERVER_URL' ] = 'https://github.com' ;
expect ( cacheUtils . isGhes ()). toBeFalsy ();
});
it ( 'returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL' , async () => {
process . env [ 'GITHUB_SERVER_URL' ] = 'https://contoso.ghe.com' ;
expect ( cacheUtils . isGhes ()). toBeFalsy ();
});
it ( 'returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix' , async () => {
process . env [ 'GITHUB_SERVER_URL' ] = 'https://mock-github.localhost' ;
expect ( cacheUtils . isGhes ()). toBeFalsy ();
});
it ( 'returns true when the GITHUB_SERVER_URL environment variable is set to some other URL' , async () => {
process . env [ 'GITHUB_SERVER_URL' ] = 'https://src.onpremise.fabrikam.com' ;
expect ( cacheUtils . isGhes ()). toBeTruthy ();
});
});