mirror of
https://github.com/actions/cache.git
synced 2026-06-06 09:04:21 +00:00
729 lines
24 KiB
TypeScript
729 lines
24 KiB
TypeScript
import * as cache from "@actions/cache";
|
|
import * as core from "@actions/core";
|
|
|
|
import { Events, Inputs, RefKey } from "../src/constants";
|
|
import { restoreImpl } from "../src/restoreImpl";
|
|
import { StateProvider } from "../src/stateProvider";
|
|
import * as actionUtils from "../src/utils/actionUtils";
|
|
import * as testUtils from "../src/utils/testUtils";
|
|
|
|
jest.mock("../src/utils/actionUtils");
|
|
|
|
beforeAll(() => {
|
|
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
|
|
(key, cacheResult) => {
|
|
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
|
return actualUtils.isExactKeyMatch(key, cacheResult);
|
|
}
|
|
);
|
|
|
|
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
|
|
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
|
return actualUtils.isValidEvent();
|
|
});
|
|
|
|
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
|
|
(name, options) => {
|
|
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
|
return actualUtils.getInputAsArray(name, options);
|
|
}
|
|
);
|
|
|
|
jest.spyOn(actionUtils, "getInputAsBool").mockImplementation(
|
|
(name, options) => {
|
|
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
|
return actualUtils.getInputAsBool(name, options);
|
|
}
|
|
);
|
|
|
|
jest.spyOn(actionUtils, "getPathValidationInput").mockImplementation(() => {
|
|
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
|
return actualUtils.getPathValidationInput();
|
|
});
|
|
|
|
jest.spyOn(actionUtils, "logWarning").mockImplementation(message => {
|
|
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
|
return actualUtils.logWarning(message);
|
|
});
|
|
});
|
|
|
|
beforeEach(() => {
|
|
jest.restoreAllMocks();
|
|
process.env[Events.Key] = Events.Push;
|
|
process.env[RefKey] = "refs/heads/feature-branch";
|
|
|
|
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
|
|
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
|
|
() => true
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
testUtils.clearInputs();
|
|
delete process.env[Events.Key];
|
|
delete process.env[RefKey];
|
|
});
|
|
|
|
test("restore with invalid event outputs warning", async () => {
|
|
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const invalidEvent = "commit_comment";
|
|
process.env[Events.Key] = invalidEvent;
|
|
delete process.env[RefKey];
|
|
await restoreImpl(new StateProvider());
|
|
expect(logWarningMock).toHaveBeenCalledWith(
|
|
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
|
|
);
|
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
});
|
|
|
|
test("restore without AC available should no-op", async () => {
|
|
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
|
|
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
|
|
() => false
|
|
);
|
|
|
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
|
|
});
|
|
|
|
test("restore on GHES without AC available should no-op", async () => {
|
|
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
|
|
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
|
|
() => false
|
|
);
|
|
|
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
|
|
});
|
|
|
|
test("restore on GHES with AC available ", async () => {
|
|
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
testUtils.setInputs({
|
|
path: path,
|
|
key,
|
|
enableCrossOsArchive: false
|
|
});
|
|
|
|
const infoMock = jest.spyOn(core, "info");
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const stateMock = jest.spyOn(core, "saveState");
|
|
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
|
|
const restoreCacheMock = jest
|
|
.spyOn(cache, "restoreCache")
|
|
.mockImplementationOnce(() => {
|
|
return Promise.resolve(key);
|
|
});
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
[path],
|
|
key,
|
|
[],
|
|
{
|
|
lookupOnly: false,
|
|
pathValidation: "warn"
|
|
},
|
|
false
|
|
);
|
|
|
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
|
|
|
|
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
|
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
});
|
|
|
|
test("restore with no path should fail", async () => {
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
await restoreImpl(new StateProvider());
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
|
// this input isn't necessary for restore b/c tarball contains entries relative to workspace
|
|
expect(failedMock).not.toHaveBeenCalledWith(
|
|
"Input required and not supplied: path"
|
|
);
|
|
});
|
|
|
|
test("restore with no key", async () => {
|
|
testUtils.setInput(Inputs.Path, "node_modules");
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
await restoreImpl(new StateProvider());
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
|
expect(failedMock).toHaveBeenCalledWith(
|
|
"Input required and not supplied: key"
|
|
);
|
|
});
|
|
|
|
test("restore with too many keys should fail", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
const restoreKeys = [...Array(20).keys()].map(x => x.toString());
|
|
testUtils.setInputs({
|
|
path: path,
|
|
key,
|
|
restoreKeys,
|
|
enableCrossOsArchive: false
|
|
});
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
await restoreImpl(new StateProvider());
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
[path],
|
|
key,
|
|
restoreKeys,
|
|
{
|
|
lookupOnly: false,
|
|
pathValidation: "warn"
|
|
},
|
|
false
|
|
);
|
|
expect(failedMock).toHaveBeenCalledWith(
|
|
`Key Validation Error: Keys are limited to a maximum of 10.`
|
|
);
|
|
});
|
|
|
|
test("restore with large key should fail", async () => {
|
|
const path = "node_modules";
|
|
const key = "foo".repeat(512); // Over the 512 character limit
|
|
testUtils.setInputs({
|
|
path: path,
|
|
key,
|
|
enableCrossOsArchive: false
|
|
});
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
await restoreImpl(new StateProvider());
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
[path],
|
|
key,
|
|
[],
|
|
{
|
|
lookupOnly: false,
|
|
pathValidation: "warn"
|
|
},
|
|
false
|
|
);
|
|
expect(failedMock).toHaveBeenCalledWith(
|
|
`Key Validation Error: ${key} cannot be larger than 512 characters.`
|
|
);
|
|
});
|
|
|
|
test("restore with invalid key should fail", async () => {
|
|
const path = "node_modules";
|
|
const key = "comma,comma";
|
|
testUtils.setInputs({
|
|
path: path,
|
|
key,
|
|
enableCrossOsArchive: false
|
|
});
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
await restoreImpl(new StateProvider());
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
[path],
|
|
key,
|
|
[],
|
|
{
|
|
lookupOnly: false,
|
|
pathValidation: "warn"
|
|
},
|
|
false
|
|
);
|
|
expect(failedMock).toHaveBeenCalledWith(
|
|
`Key Validation Error: ${key} cannot contain commas.`
|
|
);
|
|
});
|
|
|
|
test("restore with no cache found", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
testUtils.setInputs({
|
|
path: path,
|
|
key,
|
|
enableCrossOsArchive: false
|
|
});
|
|
|
|
const infoMock = jest.spyOn(core, "info");
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const stateMock = jest.spyOn(core, "saveState");
|
|
const restoreCacheMock = jest
|
|
.spyOn(cache, "restoreCache")
|
|
.mockImplementationOnce(() => {
|
|
return Promise.resolve(undefined);
|
|
});
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
[path],
|
|
key,
|
|
[],
|
|
{
|
|
lookupOnly: false,
|
|
pathValidation: "warn"
|
|
},
|
|
false
|
|
);
|
|
|
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
|
|
expect(infoMock).toHaveBeenCalledWith(
|
|
`Cache not found for input keys: ${key}`
|
|
);
|
|
});
|
|
|
|
test("restore with restore keys and no cache found", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
const restoreKey = "node-";
|
|
testUtils.setInputs({
|
|
path: path,
|
|
key,
|
|
restoreKeys: [restoreKey],
|
|
enableCrossOsArchive: false
|
|
});
|
|
|
|
const infoMock = jest.spyOn(core, "info");
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const stateMock = jest.spyOn(core, "saveState");
|
|
const restoreCacheMock = jest
|
|
.spyOn(cache, "restoreCache")
|
|
.mockImplementationOnce(() => {
|
|
return Promise.resolve(undefined);
|
|
});
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
[path],
|
|
key,
|
|
[restoreKey],
|
|
{
|
|
lookupOnly: false,
|
|
pathValidation: "warn"
|
|
},
|
|
false
|
|
);
|
|
|
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
|
|
expect(infoMock).toHaveBeenCalledWith(
|
|
`Cache not found for input keys: ${key}, ${restoreKey}`
|
|
);
|
|
});
|
|
|
|
test("restore with cache found for key", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
testUtils.setInputs({
|
|
path: path,
|
|
key,
|
|
enableCrossOsArchive: false
|
|
});
|
|
|
|
const infoMock = jest.spyOn(core, "info");
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const stateMock = jest.spyOn(core, "saveState");
|
|
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
|
|
const restoreCacheMock = jest
|
|
.spyOn(cache, "restoreCache")
|
|
.mockImplementationOnce(() => {
|
|
return Promise.resolve(key);
|
|
});
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
[path],
|
|
key,
|
|
[],
|
|
{
|
|
lookupOnly: false,
|
|
pathValidation: "warn"
|
|
},
|
|
false
|
|
);
|
|
|
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
|
|
|
|
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
|
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
});
|
|
|
|
test("restore with cache found for restore key", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
const restoreKey = "node-";
|
|
testUtils.setInputs({
|
|
path: path,
|
|
key,
|
|
restoreKeys: [restoreKey],
|
|
enableCrossOsArchive: false
|
|
});
|
|
|
|
const infoMock = jest.spyOn(core, "info");
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const stateMock = jest.spyOn(core, "saveState");
|
|
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
|
|
const restoreCacheMock = jest
|
|
.spyOn(cache, "restoreCache")
|
|
.mockImplementationOnce(() => {
|
|
return Promise.resolve(restoreKey);
|
|
});
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
[path],
|
|
key,
|
|
[restoreKey],
|
|
{
|
|
lookupOnly: false,
|
|
pathValidation: "warn"
|
|
},
|
|
false
|
|
);
|
|
|
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
|
|
expect(infoMock).toHaveBeenCalledWith(
|
|
`Cache restored from key: ${restoreKey}`
|
|
);
|
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
});
|
|
|
|
test("restore with lookup-only set", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
testUtils.setInputs({
|
|
path: path,
|
|
key,
|
|
lookupOnly: true
|
|
});
|
|
|
|
const infoMock = jest.spyOn(core, "info");
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const stateMock = jest.spyOn(core, "saveState");
|
|
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
|
|
const restoreCacheMock = jest
|
|
.spyOn(cache, "restoreCache")
|
|
.mockImplementationOnce(() => {
|
|
return Promise.resolve(key);
|
|
});
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
[path],
|
|
key,
|
|
[],
|
|
{
|
|
lookupOnly: true,
|
|
pathValidation: "warn"
|
|
},
|
|
false
|
|
);
|
|
|
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
|
expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", key);
|
|
expect(stateMock).toHaveBeenCalledTimes(2);
|
|
|
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
|
|
|
|
expect(infoMock).toHaveBeenCalledWith(
|
|
`Cache found and can be restored from key: ${key}`
|
|
);
|
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
});
|
|
|
|
test("restore failure with earlyExit should call process exit", async () => {
|
|
testUtils.setInput(Inputs.Path, "node_modules");
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
const processExitMock = jest.spyOn(process, "exit").mockImplementation();
|
|
|
|
// call restoreImpl with `earlyExit` set to true
|
|
await restoreImpl(new StateProvider(), true);
|
|
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
|
expect(failedMock).toHaveBeenCalledWith(
|
|
"Input required and not supplied: key"
|
|
);
|
|
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Path validation tests
|
|
//
|
|
// These tests verify that the action correctly forwards the `strict-paths`
|
|
// input to the @actions/cache toolkit and handles `CacheIntegrityError`
|
|
// rejections according to the `fail-on-cache-invalid` input.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("restore defaults strict-paths to 'warn' and forwards it to restoreCache", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
testUtils.setInputs({ path, key });
|
|
|
|
const restoreCacheMock = jest
|
|
.spyOn(cache, "restoreCache")
|
|
.mockResolvedValueOnce(key);
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
[path],
|
|
key,
|
|
[],
|
|
{ lookupOnly: false, pathValidation: "warn" },
|
|
false
|
|
);
|
|
});
|
|
|
|
test.each(["off", "warn", "error"])(
|
|
"restore forwards strict-paths value '%s' to restoreCache",
|
|
async value => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
testUtils.setInputs({ path, key, strictPaths: value });
|
|
|
|
const restoreCacheMock = jest
|
|
.spyOn(cache, "restoreCache")
|
|
.mockResolvedValueOnce(key);
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
[path],
|
|
key,
|
|
[],
|
|
{ lookupOnly: false, pathValidation: value },
|
|
false
|
|
);
|
|
}
|
|
);
|
|
|
|
test("restore falls back to 'warn' when strict-paths input is unrecognized", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
testUtils.setInputs({ path, key, strictPaths: "STRICT" });
|
|
|
|
const restoreCacheMock = jest
|
|
.spyOn(cache, "restoreCache")
|
|
.mockResolvedValueOnce(key);
|
|
// getPathValidationInput()'s call to logWarning() is intra-module so a
|
|
// spy on actionUtils.logWarning would not intercept it. Spy on core.info
|
|
// (the underlying transport for logWarning) and suppress the real
|
|
// implementation so the warning does not print into the Jest log.
|
|
const infoMock = jest
|
|
.spyOn(core, "info")
|
|
.mockImplementation(() => undefined);
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(restoreCacheMock).toHaveBeenCalledWith(
|
|
[path],
|
|
key,
|
|
[],
|
|
{ lookupOnly: false, pathValidation: "warn" },
|
|
false
|
|
);
|
|
expect(infoMock).toHaveBeenCalledWith(
|
|
expect.stringContaining("Unrecognized value for strict-paths")
|
|
);
|
|
});
|
|
|
|
test("restore treats CacheIntegrityError as a cache miss by default", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
testUtils.setInputs({ path, key, strictPaths: "error" });
|
|
|
|
const integrityError = new Error("entries escape declared paths");
|
|
integrityError.name = "CacheIntegrityError";
|
|
(integrityError as Error & { code?: string }).code = "PATH_VIOLATION";
|
|
|
|
const restoreCacheMock = jest
|
|
.spyOn(cache, "restoreCache")
|
|
.mockRejectedValueOnce(integrityError);
|
|
const setOutputMock = jest.spyOn(core, "setOutput");
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
// Suppress the real logWarning so the discarded-cache warning does not
|
|
// pollute test output. beforeEach's jest.restoreAllMocks() handles
|
|
// cross-test cleanup.
|
|
const logWarningMock = jest
|
|
.spyOn(actionUtils, "logWarning")
|
|
.mockImplementation(() => undefined);
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
// Intentionally NOT set: a discarded cache must look identical to a
|
|
// regular cache miss to downstream `if:` checks (see issue #1466).
|
|
const cacheHitCalls = setOutputMock.mock.calls.filter(
|
|
c => c[0] === "cache-hit"
|
|
);
|
|
expect(cacheHitCalls).toHaveLength(0);
|
|
expect(failedMock).not.toHaveBeenCalled();
|
|
expect(logWarningMock).toHaveBeenCalledWith(
|
|
expect.stringContaining("PATH_VIOLATION")
|
|
);
|
|
});
|
|
|
|
test("restore fails when CacheIntegrityError is raised and fail-on-cache-invalid is true", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
testUtils.setInputs({
|
|
path,
|
|
key,
|
|
strictPaths: "error",
|
|
failOnCacheInvalid: true
|
|
});
|
|
|
|
const integrityError = new Error("entry escapes workspace");
|
|
integrityError.name = "CacheIntegrityError";
|
|
(integrityError as Error & { code?: string }).code = "PATH_VIOLATION";
|
|
|
|
jest.spyOn(cache, "restoreCache").mockRejectedValueOnce(integrityError);
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(failedMock).toHaveBeenCalledTimes(1);
|
|
expect(failedMock.mock.calls[0][0]).toContain("integrity validation");
|
|
expect(failedMock.mock.calls[0][0]).toContain("PATH_VIOLATION");
|
|
});
|
|
|
|
test("restore propagates non-integrity errors normally", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
testUtils.setInputs({ path, key });
|
|
|
|
const networkError = new Error("Network timeout");
|
|
jest.spyOn(cache, "restoreCache").mockRejectedValueOnce(networkError);
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const logWarningMock = jest
|
|
.spyOn(actionUtils, "logWarning")
|
|
.mockImplementation(() => undefined);
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
expect(failedMock).toHaveBeenCalledWith("Network timeout");
|
|
expect(logWarningMock).not.toHaveBeenCalledWith(
|
|
expect.stringContaining("integrity")
|
|
);
|
|
});
|
|
|
|
test("restore parse-error integrity failure also treated as miss by default", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
testUtils.setInputs({ path, key, strictPaths: "error" });
|
|
|
|
const parseError = new Error("malformed gzip header");
|
|
parseError.name = "CacheIntegrityError";
|
|
(parseError as Error & { code?: string }).code = "PARSE_ERROR";
|
|
|
|
jest.spyOn(cache, "restoreCache").mockRejectedValueOnce(parseError);
|
|
const setOutputMock = jest.spyOn(core, "setOutput");
|
|
const failedMock = jest.spyOn(core, "setFailed");
|
|
const logWarningMock = jest
|
|
.spyOn(actionUtils, "logWarning")
|
|
.mockImplementation(() => undefined);
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
const cacheHitCalls = setOutputMock.mock.calls.filter(
|
|
c => c[0] === "cache-hit"
|
|
);
|
|
expect(cacheHitCalls).toHaveLength(0);
|
|
expect(failedMock).not.toHaveBeenCalled();
|
|
expect(logWarningMock).toHaveBeenCalledWith(
|
|
expect.stringContaining("PARSE_ERROR")
|
|
);
|
|
});
|
|
|
|
test("restore tolerates CacheIntegrityError without explicit code", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
testUtils.setInputs({ path, key, strictPaths: "error" });
|
|
|
|
const integrityError = new Error("bad archive");
|
|
integrityError.name = "CacheIntegrityError";
|
|
// intentionally no .code property
|
|
|
|
jest.spyOn(cache, "restoreCache").mockRejectedValueOnce(integrityError);
|
|
const setOutputMock = jest.spyOn(core, "setOutput");
|
|
const logWarningMock = jest
|
|
.spyOn(actionUtils, "logWarning")
|
|
.mockImplementation(() => undefined);
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
const cacheHitCalls = setOutputMock.mock.calls.filter(
|
|
c => c[0] === "cache-hit"
|
|
);
|
|
expect(cacheHitCalls).toHaveLength(0);
|
|
expect(logWarningMock).toHaveBeenCalledWith(
|
|
expect.stringContaining("unknown")
|
|
);
|
|
});
|
|
|
|
test("restore does not set cache-hit output when integrity error is rethrown", async () => {
|
|
const path = "node_modules";
|
|
const key = "node-test";
|
|
testUtils.setInputs({
|
|
path,
|
|
key,
|
|
strictPaths: "error",
|
|
failOnCacheInvalid: true
|
|
});
|
|
|
|
const integrityError = new Error("rejected");
|
|
integrityError.name = "CacheIntegrityError";
|
|
(integrityError as Error & { code?: string }).code = "PATH_VIOLATION";
|
|
|
|
jest.spyOn(cache, "restoreCache").mockRejectedValueOnce(integrityError);
|
|
const setOutputMock = jest.spyOn(core, "setOutput");
|
|
|
|
await restoreImpl(new StateProvider());
|
|
|
|
// setOutput should NOT have been called with cache-hit at all in this path
|
|
const cacheHitCalls = setOutputMock.mock.calls.filter(
|
|
c => c[0] === "cache-hit"
|
|
);
|
|
expect(cacheHitCalls).toHaveLength(0);
|
|
});
|