From fb677d1324fb844d84374f3aca3b02a4b4c663e6 Mon Sep 17 00:00:00 2001
From: Josh Gross <jogros@microsoft.com>
Date: Tue, 5 Nov 2019 17:54:37 -0500
Subject: [PATCH] Add unit tests for restore

---
 __tests__/main.test.ts    |  22 ---
 __tests__/restore.test.ts | 336 ++++++++++++++++++++++++++++++++++++++
 package-lock.json         |  99 +++++++++++
 package.json              |   2 +
 src/utils/testUtils.ts    |  22 +++
 5 files changed, 459 insertions(+), 22 deletions(-)
 delete mode 100644 __tests__/main.test.ts
 create mode 100644 __tests__/restore.test.ts

diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
deleted file mode 100644
index 074a5e7..0000000
--- a/__tests__/main.test.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as core from "@actions/core";
-
-import { Inputs } from "../src/constants";
-import run from "../src/restore";
-import * as testUtils from "../src/utils/testUtils";
-
-test("restore with no path", async () => {
-    const failedMock = jest.spyOn(core, "setFailed");
-    await run();
-    expect(failedMock).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");
-    await run();
-    expect(failedMock).toHaveBeenCalledWith(
-        "Input required and not supplied: key"
-    );
-});
diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts
new file mode 100644
index 0000000..921db36
--- /dev/null
+++ b/__tests__/restore.test.ts
@@ -0,0 +1,336 @@
+import * as core from "@actions/core";
+import * as exec from "@actions/exec";
+import * as io from "@actions/io";
+
+import * as path from "path";
+
+import * as cacheHttpClient from "../src/cacheHttpClient";
+import { Inputs } from "../src/constants";
+import { ArtifactCacheEntry } from "../src/contracts";
+import run from "../src/restore";
+import * as actionUtils from "../src/utils/actionUtils";
+import * as testUtils from "../src/utils/testUtils";
+
+jest.mock("@actions/exec");
+jest.mock("@actions/io");
+jest.mock("../src/utils/actionUtils");
+jest.mock("../src/cacheHttpClient");
+
+beforeAll(() => {
+    jest.spyOn(actionUtils, "resolvePath").mockImplementation(filePath => {
+        return path.resolve(filePath);
+    });
+
+    jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
+        (key, cacheResult) => {
+            const actualUtils = jest.requireActual("../src/utils/actionUtils");
+            return actualUtils.isExactKeyMatch(key, cacheResult);
+        }
+    );
+
+    jest.spyOn(io, "which").mockImplementation(tool => {
+        return Promise.resolve(tool);
+    });
+});
+afterEach(() => {
+    testUtils.clearInputs();
+});
+
+test("restore with no path", async () => {
+    const failedMock = jest.spyOn(core, "setFailed");
+    await run();
+    expect(failedMock).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");
+    await run();
+    expect(failedMock).toHaveBeenCalledWith(
+        "Input required and not supplied: key"
+    );
+});
+
+test("restore with too many keys", async () => {
+    const key = "node-test";
+    const restoreKeys = [...Array(20).keys()].map(x => x.toString());
+    testUtils.setInputs({
+        path: "node_modules",
+        key,
+        restoreKeys
+    });
+    const failedMock = jest.spyOn(core, "setFailed");
+    await run();
+    expect(failedMock).toHaveBeenCalledWith(
+        `Key Validation Error: Keys are limited to a maximum of 10.`
+    );
+});
+
+test("restore with large key", async () => {
+    const key = "foo".repeat(512); // Over the 512 character limit
+    testUtils.setInputs({
+        path: "node_modules",
+        key
+    });
+    const failedMock = jest.spyOn(core, "setFailed");
+    await run();
+    expect(failedMock).toHaveBeenCalledWith(
+        `Key Validation Error: ${key} cannot be larger than 512 characters.`
+    );
+});
+
+test("restore with invalid key", async () => {
+    const key = "comma,comma";
+    testUtils.setInputs({
+        path: "node_modules",
+        key
+    });
+    const failedMock = jest.spyOn(core, "setFailed");
+    await run();
+    expect(failedMock).toHaveBeenCalledWith(
+        `Key Validation Error: ${key} cannot contain commas.`
+    );
+});
+
+test("restore with no cache found", async () => {
+    const key = "node-test";
+    testUtils.setInputs({
+        path: "node_modules",
+        key
+    });
+
+    const infoMock = jest.spyOn(core, "info");
+    const warningMock = jest.spyOn(core, "warning");
+    const failedMock = jest.spyOn(core, "setFailed");
+    const stateMock = jest.spyOn(core, "saveState");
+
+    const clientMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
+    clientMock.mockImplementation(_ => {
+        return Promise.resolve(null);
+    });
+
+    await run();
+
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+    expect(warningMock).toHaveBeenCalledTimes(0);
+    expect(failedMock).toHaveBeenCalledTimes(0);
+
+    expect(infoMock).toHaveBeenCalledWith(
+        `Cache not found for input keys: ${key}.`
+    );
+});
+
+test("restore with no server error", async () => {
+    const key = "node-test";
+    testUtils.setInputs({
+        path: "node_modules",
+        key
+    });
+
+    const warningMock = jest.spyOn(core, "warning");
+    const failedMock = jest.spyOn(core, "setFailed");
+    const stateMock = jest.spyOn(core, "saveState");
+
+    const clientMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
+    clientMock.mockImplementation(_ => {
+        throw new Error("HTTP Error Occurred");
+    });
+
+    const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
+
+    await run();
+
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+
+    expect(warningMock).toHaveBeenCalledTimes(1);
+    expect(warningMock).toHaveBeenCalledWith("HTTP Error Occurred");
+
+    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+    expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
+
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("restore with restore keys no cache found", async () => {
+    const key = "node-test";
+    const restoreKey = "node-";
+    testUtils.setInputs({
+        path: "node_modules",
+        key,
+        restoreKeys: [restoreKey]
+    });
+
+    const infoMock = jest.spyOn(core, "info");
+    const warningMock = jest.spyOn(core, "warning");
+    const failedMock = jest.spyOn(core, "setFailed");
+    const stateMock = jest.spyOn(core, "saveState");
+
+    const clientMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
+    clientMock.mockImplementation(_ => {
+        return Promise.resolve(null);
+    });
+
+    await run();
+
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+    expect(warningMock).toHaveBeenCalledTimes(0);
+    expect(failedMock).toHaveBeenCalledTimes(0);
+
+    expect(infoMock).toHaveBeenCalledWith(
+        `Cache not found for input keys: ${key}, ${restoreKey}.`
+    );
+});
+
+test("restore with cache found", async () => {
+    const key = "node-test";
+    const cachePath = path.resolve("node_modules");
+    testUtils.setInputs({
+        path: "node_modules",
+        key
+    });
+
+    const infoMock = jest.spyOn(core, "info");
+    const warningMock = jest.spyOn(core, "warning");
+    const failedMock = jest.spyOn(core, "setFailed");
+    const stateMock = jest.spyOn(core, "saveState");
+
+    const cacheEntry: ArtifactCacheEntry = {
+        cacheKey: key,
+        scope: "refs/heads/master",
+        archiveLocation: "https://www.example.com/download"
+    };
+    const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
+    getCacheMock.mockImplementation(_ => {
+        return Promise.resolve(cacheEntry);
+    });
+    const tempPath = "/foo/bar";
+
+    const createTempDirectoryMock = jest.spyOn(
+        actionUtils,
+        "createTempDirectory"
+    );
+    createTempDirectoryMock.mockImplementation(() => {
+        return Promise.resolve(tempPath);
+    });
+
+    const archivePath = path.join(tempPath, "cache.tgz");
+    const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState");
+    const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache");
+
+    const fileSize = 142;
+    const getArchiveFileSizeMock = jest
+        .spyOn(actionUtils, "getArchiveFileSize")
+        .mockReturnValue(fileSize);
+
+    const mkdirMock = jest.spyOn(io, "mkdirP");
+    const execMock = jest.spyOn(exec, "exec");
+    const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
+
+    await run();
+
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+    expect(getCacheMock).toHaveBeenCalledWith([key]);
+    expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
+    expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
+    expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath);
+    expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
+    expect(mkdirMock).toHaveBeenCalledWith(cachePath);
+
+    const IS_WINDOWS = process.platform === "win32";
+    const tarArchivePath = IS_WINDOWS
+        ? archivePath.replace(/\\/g, "/")
+        : archivePath;
+    const tarCachePath = IS_WINDOWS ? cachePath.replace(/\\/g, "/") : cachePath;
+    const args = IS_WINDOWS ? ["-xz", "--force-local"] : ["-xz"];
+    args.push(...["-f", tarArchivePath, "-C", tarCachePath]);
+
+    expect(execMock).toHaveBeenCalledTimes(1);
+    expect(execMock).toHaveBeenCalledWith(`"tar"`, args);
+
+    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+    expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
+
+    expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
+    expect(warningMock).toHaveBeenCalledTimes(0);
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("restore with cache found for restore key", async () => {
+    const key = "node-test";
+    const restoreKey = "node-";
+    const cachePath = path.resolve("node_modules");
+    testUtils.setInputs({
+        path: "node_modules",
+        key,
+        restoreKeys: [restoreKey]
+    });
+
+    const infoMock = jest.spyOn(core, "info");
+    const warningMock = jest.spyOn(core, "warning");
+    const failedMock = jest.spyOn(core, "setFailed");
+    const stateMock = jest.spyOn(core, "saveState");
+
+    const cacheEntry: ArtifactCacheEntry = {
+        cacheKey: restoreKey,
+        scope: "refs/heads/master",
+        archiveLocation: "https://www.example.com/download"
+    };
+    const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
+    getCacheMock.mockImplementation(_ => {
+        return Promise.resolve(cacheEntry);
+    });
+    const tempPath = "/foo/bar";
+
+    const createTempDirectoryMock = jest.spyOn(
+        actionUtils,
+        "createTempDirectory"
+    );
+    createTempDirectoryMock.mockImplementation(() => {
+        return Promise.resolve(tempPath);
+    });
+
+    const archivePath = path.join(tempPath, "cache.tgz");
+    const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState");
+    const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache");
+
+    const fileSize = 142;
+    const getArchiveFileSizeMock = jest
+        .spyOn(actionUtils, "getArchiveFileSize")
+        .mockReturnValue(fileSize);
+
+    const mkdirMock = jest.spyOn(io, "mkdirP");
+    const execMock = jest.spyOn(exec, "exec");
+    const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
+
+    await run();
+
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+    expect(getCacheMock).toHaveBeenCalledWith([key, restoreKey]);
+    expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
+    expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
+    expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath);
+    expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
+    expect(mkdirMock).toHaveBeenCalledWith(cachePath);
+
+    const IS_WINDOWS = process.platform === "win32";
+    const tarArchivePath = IS_WINDOWS
+        ? archivePath.replace(/\\/g, "/")
+        : archivePath;
+    const tarCachePath = IS_WINDOWS ? cachePath.replace(/\\/g, "/") : cachePath;
+    const args = IS_WINDOWS ? ["-xz", "--force-local"] : ["-xz"];
+    args.push(...["-f", tarArchivePath, "-C", tarCachePath]);
+
+    expect(execMock).toHaveBeenCalledTimes(1);
+    expect(execMock).toHaveBeenCalledWith(`"tar"`, args);
+
+    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+    expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
+
+    expect(infoMock).toHaveBeenCalledWith(
+        `Cache restored from key: ${restoreKey}`
+    );
+    expect(warningMock).toHaveBeenCalledTimes(0);
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
diff --git a/package-lock.json b/package-lock.json
index a3dc4ea..26a0903 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -517,6 +517,15 @@
       "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==",
       "dev": true
     },
+    "@types/nock": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/@types/nock/-/nock-11.1.0.tgz",
+      "integrity": "sha512-jI/ewavBQ7X5178262JQR0ewicPAcJhXS/iFaNJl0VHLfyosZ/kwSrsa6VNQNSO8i9d8SqdRgOtZSOKJ/+iNMw==",
+      "dev": true,
+      "requires": {
+        "nock": "*"
+      }
+    },
     "@types/node": {
       "version": "12.6.9",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.9.tgz",
@@ -674,6 +683,12 @@
       "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
       "dev": true
     },
+    "assertion-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+      "dev": true
+    },
     "assign-symbols": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
@@ -984,6 +999,20 @@
       "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
       "dev": true
     },
+    "chai": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
+      "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
+      "dev": true,
+      "requires": {
+        "assertion-error": "^1.1.0",
+        "check-error": "^1.0.2",
+        "deep-eql": "^3.0.1",
+        "get-func-name": "^2.0.0",
+        "pathval": "^1.1.0",
+        "type-detect": "^4.0.5"
+      }
+    },
     "chalk": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -995,6 +1024,12 @@
         "supports-color": "^5.3.0"
       }
     },
+    "check-error": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
+      "dev": true
+    },
     "ci-info": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
@@ -1234,6 +1269,15 @@
       "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
       "dev": true
     },
+    "deep-eql": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+      "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+      "dev": true,
+      "requires": {
+        "type-detect": "^4.0.0"
+      }
+    },
     "deep-is": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
@@ -2261,6 +2305,12 @@
       "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
       "dev": true
     },
+    "get-func-name": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+      "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
+      "dev": true
+    },
     "get-stream": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
@@ -3675,6 +3725,37 @@
       "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
       "dev": true
     },
+    "nock": {
+      "version": "11.7.0",
+      "resolved": "https://registry.npmjs.org/nock/-/nock-11.7.0.tgz",
+      "integrity": "sha512-7c1jhHew74C33OBeRYyQENT+YXQiejpwIrEjinh6dRurBae+Ei4QjeUaPlkptIF0ZacEiVCnw8dWaxqepkiihg==",
+      "dev": true,
+      "requires": {
+        "chai": "^4.1.2",
+        "debug": "^4.1.0",
+        "json-stringify-safe": "^5.0.1",
+        "lodash": "^4.17.13",
+        "mkdirp": "^0.5.0",
+        "propagate": "^2.0.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        }
+      }
+    },
     "node-int64": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -4017,6 +4098,12 @@
         "pify": "^3.0.0"
       }
     },
+    "pathval": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
+      "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
+      "dev": true
+    },
     "performance-now": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -4090,6 +4177,12 @@
         "sisteransi": "^1.0.0"
       }
     },
+    "propagate": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
+      "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
+      "dev": true
+    },
     "psl": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz",
@@ -4965,6 +5058,12 @@
         "prelude-ls": "~1.1.2"
       }
     },
+    "type-detect": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+      "dev": true
+    },
     "typed-rest-client": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.5.0.tgz",
diff --git a/package.json b/package.json
index a235bd7..568e958 100644
--- a/package.json
+++ b/package.json
@@ -31,11 +31,13 @@
   },
   "devDependencies": {
     "@types/jest": "^24.0.13",
+    "@types/nock": "^11.1.0",
     "@types/node": "^12.0.4",
     "@types/uuid": "^3.4.5",
     "@zeit/ncc": "^0.20.5",
     "jest": "^24.8.0",
     "jest-circus": "^24.7.1",
+    "nock": "^11.7.0",
     "prettier": "1.18.2",
     "ts-jest": "^24.0.2",
     "typescript": "^3.6.4"
diff --git a/src/utils/testUtils.ts b/src/utils/testUtils.ts
index 67121c7..87cfc4f 100644
--- a/src/utils/testUtils.ts
+++ b/src/utils/testUtils.ts
@@ -1,3 +1,6 @@
+import { Inputs } from "../constants";
+
+// See: https://github.com/actions/toolkit/blob/master/packages/core/src/core.ts#L67
 function getInputName(name: string): string {
     return `INPUT_${name.replace(/ /g, "_").toUpperCase()}`;
 }
@@ -5,3 +8,22 @@ function getInputName(name: string): string {
 export function setInput(name: string, value: string) {
     process.env[getInputName(name)] = value;
 }
+
+interface CacheInput {
+    path: string;
+    key: string;
+    restoreKeys?: string[];
+}
+
+export function setInputs(input: CacheInput) {
+    setInput(Inputs.Path, input.path);
+    setInput(Inputs.Key, input.key);
+    input.restoreKeys &&
+        setInput(Inputs.RestoreKeys, input.restoreKeys.join("\n"));
+}
+
+export function clearInputs() {
+    delete process.env[getInputName(Inputs.Path)];
+    delete process.env[getInputName(Inputs.Key)];
+    delete process.env[getInputName(Inputs.RestoreKeys)];
+}