feat: initial
All checks were successful
Check build / check-dist (push) Successful in 1m24s
Run tests / Run vitest integration tests (push) Successful in 1m29s

This commit is contained in:
Maximilian Jugl 2026-04-23 09:49:39 +02:00
commit b04a255b4c
17 changed files with 6509 additions and 0 deletions

232
tests/action.test.js Normal file
View file

@ -0,0 +1,232 @@
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 });
}
});
});