diff --git a/__tests__/restoreOnly.test.ts b/__tests__/restoreOnly.test.ts new file mode 100644 index 0000000..ee9779c --- /dev/null +++ b/__tests__/restoreOnly.test.ts @@ -0,0 +1,322 @@ +import * as cache from "@actions/cache"; +import * as core from "@actions/core"; + +import { Events, Inputs, RefKey } from "../src/constants"; +import run from "../src/restoreOnly"; +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); + } + ); +}); + +beforeEach(() => { + 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 run(); + 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 outputMock = jest.spyOn(actionUtils, "setCacheHitOutput"); + + await run(); + + expect(restoreCacheMock).toHaveBeenCalledTimes(0); + expect(outputMock).toHaveBeenCalledTimes(1); + expect(outputMock).toHaveBeenCalledWith(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 outputMock = jest.spyOn(actionUtils, "setCacheHitOutput"); + + await run(); + + expect(restoreCacheMock).toHaveBeenCalledTimes(0); + expect(outputMock).toHaveBeenCalledTimes(1); + expect(outputMock).toHaveBeenCalledWith(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 + }); + + const infoMock = jest.spyOn(core, "info"); + const failedMock = jest.spyOn(core, "setFailed"); + const outputMock = jest.spyOn(core, "setOutput"); + const restoreCacheMock = jest + .spyOn(cache, "restoreCache") + .mockImplementationOnce(() => { + return Promise.resolve(key); + }); + + await run(); + + expect(restoreCacheMock).toHaveBeenCalledTimes(1); + expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []); + + expect(outputMock).toHaveBeenCalledWith("CACHE_KEY", key); + expect(outputMock).toHaveBeenCalledTimes(3); + expect(outputMock).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 run(); + 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 run(); + 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 + }); + const failedMock = jest.spyOn(core, "setFailed"); + const restoreCacheMock = jest.spyOn(cache, "restoreCache"); + await run(); + expect(restoreCacheMock).toHaveBeenCalledTimes(1); + expect(restoreCacheMock).toHaveBeenCalledWith([path], key, restoreKeys); + 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 + }); + const failedMock = jest.spyOn(core, "setFailed"); + const restoreCacheMock = jest.spyOn(cache, "restoreCache"); + await run(); + expect(restoreCacheMock).toHaveBeenCalledTimes(1); + expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []); + 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 + }); + const failedMock = jest.spyOn(core, "setFailed"); + const restoreCacheMock = jest.spyOn(cache, "restoreCache"); + await run(); + expect(restoreCacheMock).toHaveBeenCalledTimes(1); + expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []); + 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 + }); + + const infoMock = jest.spyOn(core, "info"); + const failedMock = jest.spyOn(core, "setFailed"); + const outputMock = jest.spyOn(core, "setOutput"); + const restoreCacheMock = jest + .spyOn(cache, "restoreCache") + .mockImplementationOnce(() => { + return Promise.resolve(undefined); + }); + + await run(); + + expect(restoreCacheMock).toHaveBeenCalledTimes(1); + expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []); + + expect(outputMock).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] + }); + + const infoMock = jest.spyOn(core, "info"); + const failedMock = jest.spyOn(core, "setFailed"); + const outputMock = jest.spyOn(core, "setOutput"); + const restoreCacheMock = jest + .spyOn(cache, "restoreCache") + .mockImplementationOnce(() => { + return Promise.resolve(undefined); + }); + + await run(); + + expect(restoreCacheMock).toHaveBeenCalledTimes(1); + expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]); + + expect(outputMock).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 + }); + + const infoMock = jest.spyOn(core, "info"); + const failedMock = jest.spyOn(core, "setFailed"); + const outputMock = jest.spyOn(core, "setOutput"); + const restoreCacheMock = jest + .spyOn(cache, "restoreCache") + .mockImplementationOnce(() => { + return Promise.resolve(key); + }); + + await run(); + + expect(restoreCacheMock).toHaveBeenCalledTimes(1); + expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []); + + expect(outputMock).toHaveBeenCalledWith("CACHE_KEY", key); + expect(outputMock).toHaveBeenCalledTimes(3); + expect(outputMock).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] + }); + + const infoMock = jest.spyOn(core, "info"); + const failedMock = jest.spyOn(core, "setFailed"); + const outputMock = jest.spyOn(core, "setOutput"); + const restoreCacheMock = jest + .spyOn(cache, "restoreCache") + .mockImplementationOnce(() => { + return Promise.resolve(restoreKey); + }); + + await run(); + + expect(restoreCacheMock).toHaveBeenCalledTimes(1); + expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]); + + expect(outputMock).toHaveBeenCalledWith("CACHE_KEY", key); + expect(outputMock).toHaveBeenCalledTimes(3); + expect(outputMock).toHaveBeenCalledWith("cache-hit", "false"); + expect(infoMock).toHaveBeenCalledWith( + `Cache restored from key: ${restoreKey}` + ); + expect(failedMock).toHaveBeenCalledTimes(0); +}); diff --git a/__tests__/saveOnly.test.ts b/__tests__/saveOnly.test.ts new file mode 100644 index 0000000..3c550e0 --- /dev/null +++ b/__tests__/saveOnly.test.ts @@ -0,0 +1,387 @@ +import * as cache from "@actions/cache"; +import * as core from "@actions/core"; + +import { Events, Inputs, RefKey } from "../src/constants"; +import run from "../src/save"; +import * as actionUtils from "../src/utils/actionUtils"; +import * as testUtils from "../src/utils/testUtils"; + +jest.mock("@actions/core"); +jest.mock("@actions/cache"); +jest.mock("../src/utils/actionUtils"); + +beforeAll(() => { + jest.spyOn(core, "getInput").mockImplementation((name, options) => { + return jest.requireActual("@actions/core").getInput(name, options); + }); + + jest.spyOn(actionUtils, "getInputAsArray").mockImplementation( + (name, options) => { + return jest + .requireActual("../src/utils/actionUtils") + .getInputAsArray(name, options); + } + ); + + jest.spyOn(actionUtils, "getInputAsInt").mockImplementation( + (name, options) => { + return jest + .requireActual("../src/utils/actionUtils") + .getInputAsInt(name, options); + } + ); + + jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation( + (key, cacheResult) => { + return jest + .requireActual("../src/utils/actionUtils") + .isExactKeyMatch(key, cacheResult); + } + ); + + jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => { + const actualUtils = jest.requireActual("../src/utils/actionUtils"); + return actualUtils.isValidEvent(); + }); +}); + +beforeEach(() => { + 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("save 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 run(); + 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("save with no primary key in state outputs warning", async () => { + const logWarningMock = jest.spyOn(actionUtils, "logWarning"); + const failedMock = jest.spyOn(core, "setFailed"); + + const savedCacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; + jest.spyOn(core, "getState") + // Cache Entry State + .mockImplementationOnce(() => { + return ""; + }) + // Cache Key State + .mockImplementationOnce(() => { + return savedCacheKey; + }); + const saveCacheMock = jest.spyOn(cache, "saveCache"); + + await run(); + + expect(saveCacheMock).toHaveBeenCalledTimes(0); + expect(logWarningMock).toHaveBeenCalledWith( + `Error retrieving key from state.` + ); + expect(logWarningMock).toHaveBeenCalledTimes(1); + expect(failedMock).toHaveBeenCalledTimes(0); +}); + +test("save without AC available should no-op", async () => { + jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation( + () => false + ); + + const saveCacheMock = jest.spyOn(cache, "saveCache"); + + await run(); + + expect(saveCacheMock).toHaveBeenCalledTimes(0); +}); + +test("save on ghes without AC available should no-op", async () => { + jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true); + jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation( + () => false + ); + + const saveCacheMock = jest.spyOn(cache, "saveCache"); + + await run(); + + expect(saveCacheMock).toHaveBeenCalledTimes(0); +}); + +test("save on GHES with AC available", async () => { + jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true); + const failedMock = jest.spyOn(core, "setFailed"); + + const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; + const savedCacheKey = "Linux-node-"; + + jest.spyOn(core, "getState") + // Cache Entry State + .mockImplementationOnce(() => { + return savedCacheKey; + }) + // Cache Key State + .mockImplementationOnce(() => { + return primaryKey; + }); + + const inputPath = "node_modules"; + testUtils.setInput(Inputs.Path, inputPath); + testUtils.setInput(Inputs.UploadChunkSize, "4000000"); + + const cacheId = 4; + const saveCacheMock = jest + .spyOn(cache, "saveCache") + .mockImplementationOnce(() => { + return Promise.resolve(cacheId); + }); + + await run(); + + expect(saveCacheMock).toHaveBeenCalledTimes(1); + expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey, { + uploadChunkSize: 4000000 + }); + + expect(failedMock).toHaveBeenCalledTimes(0); +}); + +test("save with exact match returns early", async () => { + const infoMock = jest.spyOn(core, "info"); + const failedMock = jest.spyOn(core, "setFailed"); + + const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; + const savedCacheKey = primaryKey; + + jest.spyOn(core, "getState") + // Cache Entry State + .mockImplementationOnce(() => { + return savedCacheKey; + }) + // Cache Key State + .mockImplementationOnce(() => { + return primaryKey; + }); + const saveCacheMock = jest.spyOn(cache, "saveCache"); + + await run(); + + expect(saveCacheMock).toHaveBeenCalledTimes(0); + expect(infoMock).toHaveBeenCalledWith( + `Cache hit occurred on the primary key ${primaryKey}, not saving cache.` + ); + expect(failedMock).toHaveBeenCalledTimes(0); +}); + +test("save with missing input outputs warning", async () => { + const logWarningMock = jest.spyOn(actionUtils, "logWarning"); + const failedMock = jest.spyOn(core, "setFailed"); + + const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; + const savedCacheKey = "Linux-node-"; + + jest.spyOn(core, "getState") + // Cache Entry State + .mockImplementationOnce(() => { + return savedCacheKey; + }) + // Cache Key State + .mockImplementationOnce(() => { + return primaryKey; + }); + const saveCacheMock = jest.spyOn(cache, "saveCache"); + + await run(); + + expect(saveCacheMock).toHaveBeenCalledTimes(0); + expect(logWarningMock).toHaveBeenCalledWith( + "Input required and not supplied: path" + ); + expect(logWarningMock).toHaveBeenCalledTimes(1); + expect(failedMock).toHaveBeenCalledTimes(0); +}); + +test("save with large cache outputs warning", async () => { + const logWarningMock = jest.spyOn(actionUtils, "logWarning"); + const failedMock = jest.spyOn(core, "setFailed"); + + const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; + const savedCacheKey = "Linux-node-"; + + jest.spyOn(core, "getState") + // Cache Entry State + .mockImplementationOnce(() => { + return savedCacheKey; + }) + // Cache Key State + .mockImplementationOnce(() => { + return primaryKey; + }); + + const inputPath = "node_modules"; + testUtils.setInput(Inputs.Path, inputPath); + + const saveCacheMock = jest + .spyOn(cache, "saveCache") + .mockImplementationOnce(() => { + throw new Error( + "Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache." + ); + }); + + await run(); + + expect(saveCacheMock).toHaveBeenCalledTimes(1); + expect(saveCacheMock).toHaveBeenCalledWith( + [inputPath], + primaryKey, + expect.anything() + ); + + expect(logWarningMock).toHaveBeenCalledTimes(1); + expect(logWarningMock).toHaveBeenCalledWith( + "Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache." + ); + expect(failedMock).toHaveBeenCalledTimes(0); +}); + +test("save with reserve cache failure outputs warning", async () => { + const logWarningMock = jest.spyOn(actionUtils, "logWarning"); + const failedMock = jest.spyOn(core, "setFailed"); + + const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; + const savedCacheKey = "Linux-node-"; + + jest.spyOn(core, "getState") + // Cache Entry State + .mockImplementationOnce(() => { + return savedCacheKey; + }) + // Cache Key State + .mockImplementationOnce(() => { + return primaryKey; + }); + + const inputPath = "node_modules"; + testUtils.setInput(Inputs.Path, inputPath); + + const saveCacheMock = jest + .spyOn(cache, "saveCache") + .mockImplementationOnce(() => { + const actualCache = jest.requireActual("@actions/cache"); + const error = new actualCache.ReserveCacheError( + `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.` + ); + throw error; + }); + + await run(); + + expect(saveCacheMock).toHaveBeenCalledTimes(1); + expect(saveCacheMock).toHaveBeenCalledWith( + [inputPath], + primaryKey, + expect.anything() + ); + + expect(logWarningMock).toHaveBeenCalledWith( + `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.` + ); + expect(logWarningMock).toHaveBeenCalledTimes(1); + expect(failedMock).toHaveBeenCalledTimes(0); +}); + +test("save with server error outputs warning", async () => { + const logWarningMock = jest.spyOn(actionUtils, "logWarning"); + const failedMock = jest.spyOn(core, "setFailed"); + + const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; + const savedCacheKey = "Linux-node-"; + + jest.spyOn(core, "getState") + // Cache Entry State + .mockImplementationOnce(() => { + return savedCacheKey; + }) + // Cache Key State + .mockImplementationOnce(() => { + return primaryKey; + }); + + const inputPath = "node_modules"; + testUtils.setInput(Inputs.Path, inputPath); + + const saveCacheMock = jest + .spyOn(cache, "saveCache") + .mockImplementationOnce(() => { + throw new Error("HTTP Error Occurred"); + }); + + await run(); + + expect(saveCacheMock).toHaveBeenCalledTimes(1); + expect(saveCacheMock).toHaveBeenCalledWith( + [inputPath], + primaryKey, + expect.anything() + ); + + expect(logWarningMock).toHaveBeenCalledTimes(1); + expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred"); + + expect(failedMock).toHaveBeenCalledTimes(0); +}); + +test("save with valid inputs uploads a cache", async () => { + const failedMock = jest.spyOn(core, "setFailed"); + + const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; + const savedCacheKey = "Linux-node-"; + + jest.spyOn(core, "getState") + // Cache Entry State + .mockImplementationOnce(() => { + return savedCacheKey; + }) + // Cache Key State + .mockImplementationOnce(() => { + return primaryKey; + }); + + const inputPath = "node_modules"; + testUtils.setInput(Inputs.Path, inputPath); + testUtils.setInput(Inputs.UploadChunkSize, "4000000"); + + const cacheId = 4; + const saveCacheMock = jest + .spyOn(cache, "saveCache") + .mockImplementationOnce(() => { + return Promise.resolve(cacheId); + }); + + await run(); + + expect(saveCacheMock).toHaveBeenCalledTimes(1); + expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey, { + uploadChunkSize: 4000000 + }); + + expect(failedMock).toHaveBeenCalledTimes(0); +});