mirror of
https://github.com/actions/toolkit.git
synced 2026-08-07 00:00:18 +02:00
* feat(cache): surface cache read-denied as a distinct restore warning Mirror the existing cache write-denied handling on the restore path. When the receiver refuses a download URL because the run's token has no readable cache scopes, it returns a twirp PermissionDenied (HTTP 403). The twirp client wraps that 403 in a generic Error, so the stable 'cache read denied:' prefix is embedded in the message rather than at the start. - Add CACHE_READ_DENIED_PREFIX and CacheReadDeniedError - Dispatch on the prefix in the restoreCacheV2 catch block (V2 only), log a policy-specific warning, and report a cache miss so the run continues - Add a test mirroring the write-denied coverage * chore(cache): trim comments, bump to 6.2.0, add RELEASES entry * refactor(cache): dispatch read-denied by error name to mirror write path Re-throw CacheReadDeniedError from an inner try/catch around GetCacheEntryDownloadURL and dispatch on typedError.name in the outer catch, matching how saveCacheV2 handles CacheWriteDeniedError. * feat(cache): handle read-denied on the v1 restore path Extend the read-denied handling to Cache Service v1 so GHES (which forces v1 via _apis/artifactcache) is covered when read-scope enforcement ships there. - Surface the receiver's error body message from getCacheEntry instead of a generic status-code error, so the cache read denied: prefix reaches callers - Re-throw CacheReadDeniedError from restoreCacheV1 and dispatch on it in the outer catch, mirroring restoreCacheV2 and the write-denied v1 handling - Add a v1 read-denied test * refactor(cache): only surface receiver body for read-denied on v1 * test(cache): assert getCacheEntry only surfaces body for read-denied * test(cache): cover non-read-denied getCacheEntry passthrough on v1 * refactor(cache): share read-denied prefix via constants to avoid drift * feat(cache): skip restore/save per ACTIONS_CACHE_MODE * test(cache): expand ACTIONS_CACHE_MODE skip coverage across v1/v2 and unknown modes * fix copilot pr feedback Co-authored-by: Copilot Autofix powered by AI <[email protected]> * docs(cache): remove internal reference from cache-mode comment * test(cache): merge redundant cache-mode skip tests and simplify read-denied handling Address PR review feedback: - Merge the duplicate restore/save skip test.each blocks into single blocks parametrized over ACTIONS_CACHE_SERVICE_V2. - Drop the redundant CacheReadDeniedError catch arms; the typed error is not an HttpClientError so it already falls through to a non-fatal warning. - Clarify why read-denied classification happens both in getCacheEntry and cache.ts (dependency-free internal module cannot import the typed error). * refactor(cache): drop redundant CacheWriteDeniedError catch arms Mirror the read-denied simplification on the save path. CacheWriteDeniedError is not an HttpClientError and its name does not match the ReserveCacheError arm, so it falls through to the same non-fatal warning. Logging behavior is unchanged (warns, never fails the run) and the exported type is still thrown internally for consumers and tests. Also refresh stale doc wording. * test(cache): collapse redundant restore getCacheEntry-failure tests The two restoreCache tests exercised the identical warning + cache-miss path now that read-denied is no longer reclassified in the catch, so merge them into one. The read-denied prefix detection that actually branches on the message is covered by getCacheEntry tests in cacheHttpClient.test.ts. --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]>
481 lines
17 KiB
TypeScript
481 lines
17 KiB
TypeScript
import * as core from '@actions/core'
|
|
import * as path from 'path'
|
|
import {saveCache} from '../src/cache'
|
|
import * as cacheHttpClient from '../src/internal/cacheHttpClient'
|
|
import * as cacheUtils from '../src/internal/cacheUtils'
|
|
import * as config from '../src/internal/config'
|
|
import {CacheFilename, CompressionMethod} from '../src/internal/constants'
|
|
import * as tar from '../src/internal/tar'
|
|
import {TypedResponse} from '@actions/http-client/lib/interfaces'
|
|
import {HttpClientError} from '@actions/http-client'
|
|
import {
|
|
ReserveCacheResponse,
|
|
ITypedResponseWithError
|
|
} from '../src/internal/contracts'
|
|
import {CacheServiceClientJSON} from '../src/generated/results/api/v1/cache.twirp-client'
|
|
|
|
jest.mock('../src/internal/cacheHttpClient')
|
|
jest.mock('../src/internal/cacheUtils')
|
|
jest.mock('../src/internal/config')
|
|
jest.mock('../src/internal/tar')
|
|
|
|
beforeAll(() => {
|
|
jest.spyOn(console, 'log').mockImplementation(() => {})
|
|
jest.spyOn(core, 'debug').mockImplementation(() => {})
|
|
jest.spyOn(core, 'info').mockImplementation(() => {})
|
|
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
|
jest.spyOn(core, 'error').mockImplementation(() => {})
|
|
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
|
|
const actualUtils = jest.requireActual('../src/internal/cacheUtils')
|
|
return actualUtils.getCacheFileName(cm)
|
|
})
|
|
jest.spyOn(cacheUtils, 'resolvePaths').mockImplementation(async filePaths => {
|
|
return filePaths.map(x => path.resolve(x))
|
|
})
|
|
jest.spyOn(cacheUtils, 'createTempDirectory').mockImplementation(async () => {
|
|
return Promise.resolve('/foo/bar')
|
|
})
|
|
// config is auto-mocked; use the real cache-mode helpers so gating reflects
|
|
// ACTIONS_CACHE_MODE and unset stays permissive.
|
|
const actualConfig = jest.requireActual('../src/internal/config')
|
|
jest
|
|
.spyOn(config, 'getCacheMode')
|
|
.mockImplementation(actualConfig.getCacheMode)
|
|
jest
|
|
.spyOn(config, 'isCacheReadable')
|
|
.mockImplementation(actualConfig.isCacheReadable)
|
|
jest
|
|
.spyOn(config, 'isCacheWritable')
|
|
.mockImplementation(actualConfig.isCacheWritable)
|
|
})
|
|
|
|
test('save with missing input should fail', async () => {
|
|
const paths: string[] = []
|
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
await expect(saveCache(paths, primaryKey)).rejects.toThrowError(
|
|
`Path Validation Error: At least one directory or file path is required`
|
|
)
|
|
})
|
|
|
|
describe('save cache-mode gating', () => {
|
|
const originalMode = process.env.ACTIONS_CACHE_MODE
|
|
const originalV2 = process.env.ACTIONS_CACHE_SERVICE_V2
|
|
|
|
const restoreEnv = (key: string, value: string | undefined): void => {
|
|
if (value === undefined) {
|
|
delete process.env[key]
|
|
} else {
|
|
process.env[key] = value
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
restoreEnv('ACTIONS_CACHE_MODE', originalMode)
|
|
restoreEnv('ACTIONS_CACHE_SERVICE_V2', originalV2)
|
|
})
|
|
|
|
// The skip short-circuits before v1/v2 dispatch, so it applies regardless of
|
|
// the ACTIONS_CACHE_SERVICE_V2 feature flag.
|
|
test.each([
|
|
['read', undefined],
|
|
['read', 'true'],
|
|
['none', undefined],
|
|
['none', 'true']
|
|
])(
|
|
"mode '%s' skips save with ACTIONS_CACHE_SERVICE_V2=%s",
|
|
async (mode, v2) => {
|
|
process.env.ACTIONS_CACHE_MODE = mode
|
|
restoreEnv('ACTIONS_CACHE_SERVICE_V2', v2)
|
|
const logInfoMock = jest.spyOn(core, 'info')
|
|
const resolvePathsMock = jest.spyOn(cacheUtils, 'resolvePaths')
|
|
|
|
const cacheId = await saveCache(['node_modules'], 'node-test')
|
|
|
|
expect(cacheId).toBe(-1)
|
|
expect(resolvePathsMock).not.toHaveBeenCalled()
|
|
expect(logInfoMock).toHaveBeenCalledTimes(1)
|
|
expect(logInfoMock).toHaveBeenCalledWith(
|
|
`Cache save skipped: the effective cache-mode '${mode}' does not permit writes.`
|
|
)
|
|
}
|
|
)
|
|
|
|
test.each(['write', 'write-only', '', 'garbage'])(
|
|
"mode '%s' does not skip save",
|
|
async mode => {
|
|
if (mode === '') {
|
|
delete process.env.ACTIONS_CACHE_MODE
|
|
} else {
|
|
process.env.ACTIONS_CACHE_MODE = mode
|
|
}
|
|
const logInfoMock = jest.spyOn(core, 'info')
|
|
const resolvePathsMock = jest.spyOn(cacheUtils, 'resolvePaths')
|
|
|
|
try {
|
|
await saveCache(['node_modules'], 'node-test')
|
|
} catch {
|
|
// Downstream client is not fully mocked here; we only assert the guard
|
|
// let execution proceed past it.
|
|
}
|
|
|
|
expect(resolvePathsMock).toHaveBeenCalled()
|
|
expect(logInfoMock).not.toHaveBeenCalledWith(
|
|
expect.stringContaining('Cache save skipped')
|
|
)
|
|
}
|
|
)
|
|
})
|
|
|
|
test('save with large cache outputs should fail', async () => {
|
|
const filePath = 'node_modules'
|
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
const cachePaths = [path.resolve(filePath)]
|
|
|
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
|
const logWarningMock = jest.spyOn(core, 'warning')
|
|
|
|
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
|
|
jest
|
|
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
|
.mockReturnValueOnce(cacheSize)
|
|
const compression = CompressionMethod.Gzip
|
|
const getCompressionMock = jest
|
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
|
.mockReturnValueOnce(Promise.resolve(compression))
|
|
|
|
const cacheId = await saveCache([filePath], primaryKey)
|
|
expect(cacheId).toBe(-1)
|
|
expect(logWarningMock).toHaveBeenCalledTimes(1)
|
|
expect(logWarningMock).toHaveBeenCalledWith(
|
|
'Failed to save: Cache size of ~11264 MB (11811160064 B) is over the 10GB limit, not saving cache.'
|
|
)
|
|
|
|
const archiveFolder = '/foo/bar'
|
|
|
|
expect(createTarMock).toHaveBeenCalledTimes(1)
|
|
expect(createTarMock).toHaveBeenCalledWith(
|
|
archiveFolder,
|
|
cachePaths,
|
|
compression
|
|
)
|
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
test('save with large cache outputs should fail in GHES with error message', async () => {
|
|
const filePath = 'node_modules'
|
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
const cachePaths = [path.resolve(filePath)]
|
|
|
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
|
const logWarningMock = jest.spyOn(core, 'warning')
|
|
|
|
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
|
|
jest
|
|
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
|
.mockReturnValueOnce(cacheSize)
|
|
const compression = CompressionMethod.Gzip
|
|
const getCompressionMock = jest
|
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
|
.mockReturnValueOnce(Promise.resolve(compression))
|
|
|
|
jest.spyOn(config, 'isGhes').mockReturnValueOnce(true)
|
|
|
|
const reserveCacheMock = jest
|
|
.spyOn(cacheHttpClient, 'reserveCache')
|
|
.mockImplementation(async () => {
|
|
const response: ITypedResponseWithError<ReserveCacheResponse> = {
|
|
statusCode: 400,
|
|
result: null,
|
|
headers: {},
|
|
error: new HttpClientError(
|
|
'The cache filesize must be between 0 and 1073741824 bytes',
|
|
400
|
|
)
|
|
}
|
|
return response
|
|
})
|
|
|
|
const cacheId = await saveCache([filePath], primaryKey)
|
|
expect(cacheId).toBe(-1)
|
|
expect(logWarningMock).toHaveBeenCalledTimes(1)
|
|
expect(logWarningMock).toHaveBeenCalledWith(
|
|
'Failed to save: The cache filesize must be between 0 and 1073741824 bytes'
|
|
)
|
|
|
|
const archiveFolder = '/foo/bar'
|
|
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
|
expect(createTarMock).toHaveBeenCalledTimes(1)
|
|
expect(createTarMock).toHaveBeenCalledWith(
|
|
archiveFolder,
|
|
cachePaths,
|
|
compression
|
|
)
|
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
test('save with large cache outputs should fail in GHES without error message', async () => {
|
|
const filePath = 'node_modules'
|
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
const cachePaths = [path.resolve(filePath)]
|
|
|
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
|
const logWarningMock = jest.spyOn(core, 'warning')
|
|
|
|
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
|
|
jest
|
|
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
|
.mockReturnValueOnce(cacheSize)
|
|
const compression = CompressionMethod.Gzip
|
|
const getCompressionMock = jest
|
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
|
.mockReturnValueOnce(Promise.resolve(compression))
|
|
|
|
jest.spyOn(config, 'isGhes').mockReturnValueOnce(true)
|
|
|
|
const reserveCacheMock = jest
|
|
.spyOn(cacheHttpClient, 'reserveCache')
|
|
.mockImplementation(async () => {
|
|
const response: ITypedResponseWithError<ReserveCacheResponse> = {
|
|
statusCode: 400,
|
|
result: null,
|
|
headers: {}
|
|
}
|
|
return response
|
|
})
|
|
|
|
const cacheId = await saveCache([filePath], primaryKey)
|
|
expect(cacheId).toBe(-1)
|
|
expect(logWarningMock).toHaveBeenCalledTimes(1)
|
|
expect(logWarningMock).toHaveBeenCalledWith(
|
|
'Failed to save: Cache size of ~11264 MB (11811160064 B) is over the data cap limit, not saving cache.'
|
|
)
|
|
|
|
const archiveFolder = '/foo/bar'
|
|
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
|
expect(createTarMock).toHaveBeenCalledTimes(1)
|
|
expect(createTarMock).toHaveBeenCalledWith(
|
|
archiveFolder,
|
|
cachePaths,
|
|
compression
|
|
)
|
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
test('save with reserve cache failure should fail', async () => {
|
|
const paths = ['node_modules']
|
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
const logInfoMock = jest.spyOn(core, 'info')
|
|
|
|
const reserveCacheMock = jest
|
|
.spyOn(cacheHttpClient, 'reserveCache')
|
|
.mockImplementation(async () => {
|
|
const response: TypedResponse<ReserveCacheResponse> = {
|
|
statusCode: 500,
|
|
result: null,
|
|
headers: {}
|
|
}
|
|
return response
|
|
})
|
|
|
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
|
const saveCacheMock = jest.spyOn(cacheHttpClient, 'saveCache')
|
|
const compression = CompressionMethod.Zstd
|
|
const getCompressionMock = jest
|
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
|
.mockReturnValueOnce(Promise.resolve(compression))
|
|
|
|
const cacheId = await saveCache(paths, primaryKey)
|
|
expect(cacheId).toBe(-1)
|
|
expect(logInfoMock).toHaveBeenCalledTimes(1)
|
|
expect(logInfoMock).toHaveBeenCalledWith(
|
|
`Failed to save: Unable to reserve cache with key ${primaryKey}, another job may be creating this cache. More details: undefined`
|
|
)
|
|
|
|
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
|
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, paths, {
|
|
cacheSize: undefined,
|
|
compressionMethod: compression,
|
|
enableCrossOsArchive: false
|
|
})
|
|
expect(createTarMock).toHaveBeenCalledTimes(1)
|
|
expect(saveCacheMock).toHaveBeenCalledTimes(0)
|
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
test('save with reserve cache denied by read-only token logs warning (not info)', async () => {
|
|
// V1 path: when the legacy ReserveCache REST call returns an error message
|
|
// starting with the stable `cache write denied:` prefix, the toolkit must
|
|
// surface it as a `core.warning` (not the usual `core.info` used for the
|
|
// generic "another job may be creating this cache" contention case).
|
|
const paths = ['node_modules']
|
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
const deniedMessage =
|
|
'cache write denied: read-only token issued for untrusted trigger'
|
|
const logInfoMock = jest.spyOn(core, 'info')
|
|
const logWarningMock = jest.spyOn(core, 'warning')
|
|
|
|
const reserveCacheMock = jest
|
|
.spyOn(cacheHttpClient, 'reserveCache')
|
|
.mockImplementation(async () => {
|
|
const response: ITypedResponseWithError<ReserveCacheResponse> = {
|
|
statusCode: 403,
|
|
result: null,
|
|
headers: {},
|
|
error: new HttpClientError(deniedMessage, 403)
|
|
}
|
|
return response
|
|
})
|
|
|
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
|
const saveCacheMock = jest.spyOn(cacheHttpClient, 'saveCache')
|
|
const compression = CompressionMethod.Zstd
|
|
const getCompressionMock = jest
|
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
|
.mockReturnValueOnce(Promise.resolve(compression))
|
|
|
|
const cacheId = await saveCache(paths, primaryKey)
|
|
expect(cacheId).toBe(-1)
|
|
|
|
// The generic "another job may be creating this cache" info log MUST NOT
|
|
// fire — this is a policy denial, not a contention case.
|
|
expect(logInfoMock).not.toHaveBeenCalledWith(
|
|
expect.stringContaining('another job may be creating this cache')
|
|
)
|
|
// A single warning carrying the stable prefix is what the customer sees.
|
|
expect(logWarningMock).toHaveBeenCalledWith(
|
|
`Failed to save: Unable to reserve cache with key ${primaryKey}. More details: ${deniedMessage}`
|
|
)
|
|
|
|
expect(logWarningMock).toHaveBeenCalledTimes(1)
|
|
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
|
expect(createTarMock).toHaveBeenCalledTimes(1)
|
|
expect(saveCacheMock).toHaveBeenCalledTimes(0)
|
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
test('save with server error should fail', async () => {
|
|
const filePath = 'node_modules'
|
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
const logErrorMock = jest.spyOn(core, 'error')
|
|
|
|
// Mock cache service version to V2
|
|
const getCacheServiceVersionMock = jest
|
|
.spyOn(config, 'getCacheServiceVersion')
|
|
.mockReturnValue('v2')
|
|
|
|
// Mock V2 CreateCacheEntry to succeed
|
|
const createCacheEntryMock = jest
|
|
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
|
|
.mockReturnValue(
|
|
Promise.resolve({
|
|
ok: true,
|
|
signedUploadUrl: 'https://blob-storage.local?signed=true',
|
|
message: ''
|
|
})
|
|
)
|
|
|
|
// Mock the FinalizeCacheEntryUpload to succeed (since the error should happen in saveCache)
|
|
jest
|
|
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
|
|
.mockReturnValue(
|
|
Promise.resolve({ok: true, entryId: '4', message: 'Success'})
|
|
)
|
|
|
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
|
|
|
// Mock the saveCache call to throw a server error
|
|
const saveCacheMock = jest
|
|
.spyOn(cacheHttpClient, 'saveCache')
|
|
.mockImplementationOnce(() => {
|
|
throw new HttpClientError('HTTP Error Occurred', 500)
|
|
})
|
|
|
|
const compression = CompressionMethod.Zstd
|
|
const getCompressionMock = jest
|
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
|
.mockReturnValueOnce(Promise.resolve(compression))
|
|
|
|
await saveCache([filePath], primaryKey)
|
|
|
|
expect(logErrorMock).toHaveBeenCalledTimes(1)
|
|
expect(logErrorMock).toHaveBeenCalledWith(
|
|
'Failed to save: HTTP Error Occurred'
|
|
)
|
|
|
|
expect(createCacheEntryMock).toHaveBeenCalledTimes(1)
|
|
const archiveFolder = '/foo/bar'
|
|
const cachePaths = [path.resolve(filePath)]
|
|
expect(createTarMock).toHaveBeenCalledTimes(1)
|
|
expect(createTarMock).toHaveBeenCalledWith(
|
|
archiveFolder,
|
|
cachePaths,
|
|
compression
|
|
)
|
|
expect(saveCacheMock).toHaveBeenCalledTimes(1)
|
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
|
|
|
// Restore the getCacheServiceVersion mock to its original state
|
|
getCacheServiceVersionMock.mockRestore()
|
|
})
|
|
|
|
test('save with valid inputs uploads a cache', async () => {
|
|
const filePath = 'node_modules'
|
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
const cachePaths = [path.resolve(filePath)]
|
|
|
|
const cacheId = 4
|
|
const reserveCacheMock = jest
|
|
.spyOn(cacheHttpClient, 'reserveCache')
|
|
.mockImplementation(async () => {
|
|
const response: TypedResponse<ReserveCacheResponse> = {
|
|
statusCode: 500,
|
|
result: {cacheId},
|
|
headers: {}
|
|
}
|
|
return response
|
|
})
|
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
|
|
|
const saveCacheMock = jest.spyOn(cacheHttpClient, 'saveCache')
|
|
const compression = CompressionMethod.Zstd
|
|
const getCompressionMock = jest
|
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
|
.mockReturnValue(Promise.resolve(compression))
|
|
|
|
await saveCache([filePath], primaryKey)
|
|
|
|
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
|
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], {
|
|
cacheSize: undefined,
|
|
compressionMethod: compression,
|
|
enableCrossOsArchive: false
|
|
})
|
|
const archiveFolder = '/foo/bar'
|
|
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
|
|
expect(createTarMock).toHaveBeenCalledTimes(1)
|
|
expect(createTarMock).toHaveBeenCalledWith(
|
|
archiveFolder,
|
|
cachePaths,
|
|
compression
|
|
)
|
|
expect(saveCacheMock).toHaveBeenCalledTimes(1)
|
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
|
cacheId,
|
|
archiveFile,
|
|
'',
|
|
undefined
|
|
)
|
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
test('save with non existing path should not save cache', async () => {
|
|
const path = 'node_modules'
|
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
jest.spyOn(cacheUtils, 'resolvePaths').mockImplementation(async () => {
|
|
return []
|
|
})
|
|
await expect(saveCache([path], primaryKey)).rejects.toThrowError(
|
|
`Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.`
|
|
)
|
|
})
|