232 lines
6.5 KiB
JavaScript
232 lines
6.5 KiB
JavaScript
import { afterAll, beforeAll, beforeEach, afterEach, describe, vi, it, expect } from "vitest";
|
|
import { GenericContainer, Wait } from "testcontainers";
|
|
|
|
import * as core from "@actions/core";
|
|
|
|
import fs from "fs/promises";
|
|
import os from "os";
|
|
import path from "path";
|
|
import crypto from "crypto";
|
|
|
|
import { restore } from "../src/restore.js";
|
|
import { save } from "../src/save.js";
|
|
|
|
vi.mock("@actions/core");
|
|
|
|
const fileSha256 = async (filePath) => {
|
|
const buf = await fs.readFile(filePath);
|
|
return crypto.createHash("sha256").update(buf).digest("hex");
|
|
};
|
|
|
|
describe("S3 Cache Action Integration Tests", () => {
|
|
// defaults
|
|
const s3bucket = "integration-test";
|
|
const s3region = "us-east-1";
|
|
|
|
// env leak prevention
|
|
const originalEnv = process.env;
|
|
|
|
// endpoint generated by testcontainers
|
|
let s3mockContainer;
|
|
let s3endpoint;
|
|
|
|
// tmpdirs used by runner
|
|
let runnerWorkspace;
|
|
let runnerTemp;
|
|
|
|
// tmpdirs and files used by action
|
|
let testDataFolder;
|
|
let testDataFile;
|
|
|
|
// mocked inputs and states
|
|
let inputs;
|
|
let states;
|
|
|
|
beforeAll(async () => {
|
|
process.env = { ...originalEnv };
|
|
|
|
// gnutar fix for macos
|
|
if (os.platform() == "darwin") {
|
|
const gnuTarPath =
|
|
os.arch() === "arm64" ? "/opt/homebrew/opt/gnu-tar/libexec/gnubin" : "/usr/local/opt/gnu-tar/libexec/gnubin";
|
|
|
|
process.env.PATH = `${gnuTarPath}:${process.env.PATH}`;
|
|
}
|
|
|
|
if (process.env.S3_ENDPOINT) {
|
|
s3endpoint = process.env.S3_ENDPOINT;
|
|
} else {
|
|
// start testcontainers
|
|
s3mockContainer = await new GenericContainer("adobe/s3mock:5.0.0")
|
|
.withExposedPorts(9090)
|
|
.withEnvironment({
|
|
COM_ADOBE_TESTING_S3MOCK_STORE_INITIAL_BUCKETS: s3bucket,
|
|
COM_ADOBE_TESTING_S3MOCK_STORE_REGION: s3region,
|
|
})
|
|
.withWaitStrategy(Wait.forLogMessage(/.*Started S3MockApplication\.Companion in.*/))
|
|
.start();
|
|
|
|
s3endpoint = `http://${s3mockContainer.getHost()}:${s3mockContainer.getMappedPort(9090)}`;
|
|
}
|
|
|
|
// setup runner environment
|
|
const workspacePrefix = path.join(os.tmpdir(), "runner-workspace-");
|
|
const tempPrefix = path.join(os.tmpdir(), "runner-temp-");
|
|
|
|
runnerWorkspace = await fs.mkdtemp(workspacePrefix);
|
|
runnerTemp = await fs.mkdtemp(tempPrefix);
|
|
|
|
process.env.GITHUB_WORKSPACE = runnerWorkspace;
|
|
process.env.RUNNER_TEMP = runnerTemp;
|
|
}, 60000);
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
|
|
// create new data folder for each test run
|
|
const testDataPrefix = path.join(os.tmpdir(), "runner-data-");
|
|
testDataFolder = await fs.mkdtemp(testDataPrefix);
|
|
|
|
// default mock inputs
|
|
inputs = {
|
|
key: `test-${crypto.randomUUID()}`,
|
|
path: testDataFolder,
|
|
"s3-bucket": s3bucket,
|
|
"s3-endpoint": s3endpoint,
|
|
"s3-access-key": "mock-access",
|
|
"s3-secret-key": "mock-secret",
|
|
"s3-region": s3region,
|
|
"lookup-only": "false",
|
|
"fail-on-cache-miss": "false",
|
|
"restore-keys": "",
|
|
};
|
|
|
|
// mock state
|
|
states = {};
|
|
|
|
// mock function calls
|
|
vi.mocked(core.getInput).mockImplementation((name) => inputs[name] || "");
|
|
vi.mocked(core.getMultilineInput).mockImplementation((name) => {
|
|
if (name === "path") {
|
|
return [inputs["path"]];
|
|
}
|
|
|
|
if (name === "restore-keys") {
|
|
return inputs["restore-keys"] ? inputs["restore-keys"].split("\n") : [];
|
|
}
|
|
|
|
return [];
|
|
});
|
|
vi.mocked(core.getState).mockImplementation((name) => states[name] || "");
|
|
vi.mocked(core.saveState).mockImplementation((name, value) => {
|
|
states[name] = value;
|
|
});
|
|
|
|
// generate test data file with random data
|
|
testDataFile = path.join(testDataFolder, `${crypto.randomUUID()}`);
|
|
await fs.writeFile(testDataFile, crypto.randomBytes(4 * 1024 * 1024));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fs.rm(testDataFolder, { force: true, recursive: true });
|
|
});
|
|
|
|
it("should result in cache-miss when key is not found", async () => {
|
|
await restore();
|
|
|
|
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
|
|
expect(core.saveState).toHaveBeenCalledWith("exactMatch", "false");
|
|
});
|
|
|
|
it("should save the cache successfully", async () => {
|
|
await save();
|
|
|
|
expect(core.info).toHaveBeenCalledWith("Upload complete");
|
|
});
|
|
|
|
it("should restore matched cache exactly", async () => {
|
|
const originalChecksum = await fileSha256(testDataFile);
|
|
await save();
|
|
|
|
await fs.rm(testDataFile);
|
|
|
|
await restore();
|
|
|
|
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
|
|
expect(core.saveState).toHaveBeenCalledWith("exactMatch", "true");
|
|
|
|
const restoredChecksum = await fileSha256(testDataFile);
|
|
expect(restoredChecksum).toBe(originalChecksum);
|
|
});
|
|
|
|
it("should skip save if exact match exists", async () => {
|
|
states["exactMatch"] = "true";
|
|
|
|
await save();
|
|
|
|
expect(core.info).toHaveBeenCalledWith("Exact match found, skipping cache upload");
|
|
});
|
|
|
|
it("should match prefix using restore-keys", async () => {
|
|
inputs["key"] = "test-key-12345";
|
|
|
|
const originalChecksum = await fileSha256(testDataFile);
|
|
await save();
|
|
|
|
await fs.rm(testDataFile);
|
|
|
|
inputs["key"] = "completely-different-key";
|
|
inputs["restore-keys"] = "test-key-";
|
|
|
|
await restore();
|
|
|
|
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
|
|
expect(core.saveState).toHaveBeenCalledWith("exactMatch", "false");
|
|
|
|
const restoredChecksum = await fileSha256(testDataFile);
|
|
expect(restoredChecksum).toBe(originalChecksum);
|
|
});
|
|
|
|
it("should respect lookup-only and not extract files", async () => {
|
|
await save();
|
|
await fs.rm(testDataFile);
|
|
|
|
inputs["lookup-only"] = "true";
|
|
await restore();
|
|
|
|
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "true");
|
|
expect(core.saveState).toHaveBeenCalledWith("exactMatch", "true");
|
|
|
|
await expect(fs.readFile(testDataFile)).rejects.toThrow(/ENOENT/);
|
|
});
|
|
|
|
it("should respect fail-on-cache-miss", async () => {
|
|
inputs["fail-on-cache-miss"] = "true";
|
|
|
|
await restore();
|
|
|
|
expect(core.setOutput).toHaveBeenCalledWith("cache-hit", "false");
|
|
expect(core.setFailed).toHaveBeenCalledWith("No matching cache key found.");
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// reset env
|
|
process.env = originalEnv;
|
|
|
|
if (s3mockContainer) {
|
|
await s3mockContainer.stop();
|
|
}
|
|
|
|
if (runnerWorkspace) {
|
|
await fs.rm(runnerWorkspace, { recursive: true, force: true });
|
|
}
|
|
|
|
if (runnerTemp) {
|
|
await fs.rm(runnerTemp, { recursive: true, force: true });
|
|
}
|
|
|
|
if (testDataFolder) {
|
|
await fs.rm(testDataFolder, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|