mirror of https://github.com/actions/cache.git
233 lines
8.5 KiB
TypeScript
233 lines
8.5 KiB
TypeScript
|
import * as core from "@actions/core";
|
||
|
import * as fs from "fs";
|
||
|
import nock from "nock";
|
||
|
import * as path from "path";
|
||
|
|
||
|
import { DownloadValidationError, restoreCache } from "../src/custom/cache";
|
||
|
import { downloadCacheHttpClientConcurrent } from "../src/custom/downloadUtils";
|
||
|
|
||
|
// Mock the core module
|
||
|
jest.mock("@actions/core");
|
||
|
|
||
|
// Mock fs for file size checks
|
||
|
jest.mock("fs", () => ({
|
||
|
...jest.requireActual("fs"),
|
||
|
promises: {
|
||
|
...jest.requireActual("fs").promises,
|
||
|
open: jest.fn()
|
||
|
}
|
||
|
}));
|
||
|
|
||
|
describe("Download Validation", () => {
|
||
|
const testArchivePath = "/tmp/test-cache.tar.gz";
|
||
|
const testUrl = "https://example.com/cache.tar.gz";
|
||
|
|
||
|
beforeEach(() => {
|
||
|
jest.clearAllMocks();
|
||
|
nock.cleanAll();
|
||
|
});
|
||
|
|
||
|
afterEach(() => {
|
||
|
nock.cleanAll();
|
||
|
});
|
||
|
|
||
|
describe("downloadCacheHttpClientConcurrent", () => {
|
||
|
it("should validate downloaded size matches expected content-length", async () => {
|
||
|
const expectedSize = 1024;
|
||
|
const mockFileDescriptor = {
|
||
|
write: jest.fn().mockResolvedValue(undefined),
|
||
|
close: jest.fn().mockResolvedValue(undefined)
|
||
|
};
|
||
|
|
||
|
(fs.promises.open as jest.Mock).mockResolvedValue(
|
||
|
mockFileDescriptor
|
||
|
);
|
||
|
|
||
|
// Mock the initial range request to get content length
|
||
|
nock("https://example.com")
|
||
|
.get("/cache.tar.gz")
|
||
|
.reply(206, "partial content", {
|
||
|
"content-range": `bytes 0-1/${expectedSize}`
|
||
|
});
|
||
|
|
||
|
// Mock the actual content download with wrong size
|
||
|
nock("https://example.com")
|
||
|
.get("/cache.tar.gz")
|
||
|
.reply(206, Buffer.alloc(512), {
|
||
|
// Return only 512 bytes instead of 1024
|
||
|
"content-range": "bytes 0-511/1024"
|
||
|
});
|
||
|
|
||
|
await expect(
|
||
|
downloadCacheHttpClientConcurrent(testUrl, testArchivePath, {
|
||
|
timeoutInMs: 30000,
|
||
|
partSize: 1024
|
||
|
})
|
||
|
).rejects.toThrow(
|
||
|
"Download validation failed: Expected 1024 bytes but downloaded 512 bytes"
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it("should succeed when downloaded size matches expected", async () => {
|
||
|
const expectedSize = 1024;
|
||
|
const testContent = Buffer.alloc(expectedSize);
|
||
|
const mockFileDescriptor = {
|
||
|
write: jest.fn().mockResolvedValue(undefined),
|
||
|
close: jest.fn().mockResolvedValue(undefined)
|
||
|
};
|
||
|
|
||
|
(fs.promises.open as jest.Mock).mockResolvedValue(
|
||
|
mockFileDescriptor
|
||
|
);
|
||
|
|
||
|
// Mock the initial range request
|
||
|
nock("https://example.com")
|
||
|
.get("/cache.tar.gz")
|
||
|
.reply(206, "partial content", {
|
||
|
"content-range": `bytes 0-1/${expectedSize}`
|
||
|
});
|
||
|
|
||
|
// Mock the actual content download with correct size
|
||
|
nock("https://example.com")
|
||
|
.get("/cache.tar.gz")
|
||
|
.reply(206, testContent, {
|
||
|
"content-range": `bytes 0-${
|
||
|
expectedSize - 1
|
||
|
}/${expectedSize}`
|
||
|
});
|
||
|
|
||
|
await expect(
|
||
|
downloadCacheHttpClientConcurrent(testUrl, testArchivePath, {
|
||
|
timeoutInMs: 30000,
|
||
|
partSize: expectedSize
|
||
|
})
|
||
|
).resolves.not.toThrow();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe("restoreCache validation", () => {
|
||
|
beforeEach(() => {
|
||
|
// Mock environment variables for S3 backend
|
||
|
process.env.RUNS_ON_S3_BUCKET_CACHE = "test-bucket";
|
||
|
process.env.RUNS_ON_AWS_REGION = "us-east-1";
|
||
|
});
|
||
|
|
||
|
afterEach(() => {
|
||
|
delete process.env.RUNS_ON_S3_BUCKET_CACHE;
|
||
|
delete process.env.RUNS_ON_AWS_REGION;
|
||
|
});
|
||
|
|
||
|
it("should throw DownloadValidationError for empty files", async () => {
|
||
|
// Mock the cache lookup to return a valid cache entry
|
||
|
const mockCacheHttpClient = require("../src/custom/backend");
|
||
|
jest.spyOn(mockCacheHttpClient, "getCacheEntry").mockResolvedValue({
|
||
|
cacheKey: "test-key",
|
||
|
archiveLocation: "https://s3.example.com/cache.tar.gz"
|
||
|
});
|
||
|
|
||
|
// Mock the download to succeed
|
||
|
jest.spyOn(mockCacheHttpClient, "downloadCache").mockResolvedValue(
|
||
|
undefined
|
||
|
);
|
||
|
|
||
|
// Mock utils to return 0 file size (empty file)
|
||
|
const mockUtils = require("@actions/cache/lib/internal/cacheUtils");
|
||
|
jest.spyOn(mockUtils, "getArchiveFileSizeInBytes").mockReturnValue(
|
||
|
0
|
||
|
);
|
||
|
jest.spyOn(mockUtils, "createTempDirectory").mockResolvedValue(
|
||
|
"/tmp"
|
||
|
);
|
||
|
jest.spyOn(mockUtils, "getCacheFileName").mockReturnValue(
|
||
|
"cache.tar.gz"
|
||
|
);
|
||
|
|
||
|
const coreSpy = jest.spyOn(core, "warning");
|
||
|
|
||
|
const result = await restoreCache(["/test/path"], "test-key");
|
||
|
|
||
|
expect(result).toBeUndefined(); // Should return undefined on validation failure
|
||
|
expect(coreSpy).toHaveBeenCalledWith(
|
||
|
expect.stringContaining(
|
||
|
"Cache download validation failed: Downloaded cache archive is empty"
|
||
|
)
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it("should throw DownloadValidationError for files too small", async () => {
|
||
|
// Mock the cache lookup to return a valid cache entry
|
||
|
const mockCacheHttpClient = require("../src/custom/backend");
|
||
|
jest.spyOn(mockCacheHttpClient, "getCacheEntry").mockResolvedValue({
|
||
|
cacheKey: "test-key",
|
||
|
archiveLocation: "https://s3.example.com/cache.tar.gz"
|
||
|
});
|
||
|
|
||
|
// Mock the download to succeed
|
||
|
jest.spyOn(mockCacheHttpClient, "downloadCache").mockResolvedValue(
|
||
|
undefined
|
||
|
);
|
||
|
|
||
|
// Mock utils to return small file size (less than 512 bytes)
|
||
|
const mockUtils = require("@actions/cache/lib/internal/cacheUtils");
|
||
|
jest.spyOn(mockUtils, "getArchiveFileSizeInBytes").mockReturnValue(
|
||
|
100
|
||
|
);
|
||
|
jest.spyOn(mockUtils, "createTempDirectory").mockResolvedValue(
|
||
|
"/tmp"
|
||
|
);
|
||
|
jest.spyOn(mockUtils, "getCacheFileName").mockReturnValue(
|
||
|
"cache.tar.gz"
|
||
|
);
|
||
|
|
||
|
const coreSpy = jest.spyOn(core, "warning");
|
||
|
|
||
|
const result = await restoreCache(["/test/path"], "test-key");
|
||
|
|
||
|
expect(result).toBeUndefined(); // Should return undefined on validation failure
|
||
|
expect(coreSpy).toHaveBeenCalledWith(
|
||
|
expect.stringContaining(
|
||
|
"Cache download validation failed: Downloaded cache archive is too small (100 bytes)"
|
||
|
)
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it("should succeed with valid file size", async () => {
|
||
|
// Mock the cache lookup to return a valid cache entry
|
||
|
const mockCacheHttpClient = require("../src/custom/backend");
|
||
|
jest.spyOn(mockCacheHttpClient, "getCacheEntry").mockResolvedValue({
|
||
|
cacheKey: "test-key",
|
||
|
archiveLocation: "https://s3.example.com/cache.tar.gz"
|
||
|
});
|
||
|
|
||
|
// Mock the download to succeed
|
||
|
jest.spyOn(mockCacheHttpClient, "downloadCache").mockResolvedValue(
|
||
|
undefined
|
||
|
);
|
||
|
|
||
|
// Mock utils to return valid file size (>= 512 bytes)
|
||
|
const mockUtils = require("@actions/cache/lib/internal/cacheUtils");
|
||
|
jest.spyOn(mockUtils, "getArchiveFileSizeInBytes").mockReturnValue(
|
||
|
1024
|
||
|
);
|
||
|
jest.spyOn(mockUtils, "createTempDirectory").mockResolvedValue(
|
||
|
"/tmp"
|
||
|
);
|
||
|
jest.spyOn(mockUtils, "getCacheFileName").mockReturnValue(
|
||
|
"cache.tar.gz"
|
||
|
);
|
||
|
jest.spyOn(mockUtils, "getCompressionMethod").mockResolvedValue(
|
||
|
"gzip"
|
||
|
);
|
||
|
|
||
|
// Mock tar operations
|
||
|
const mockTar = require("@actions/cache/lib/internal/tar");
|
||
|
jest.spyOn(mockTar, "extractTar").mockResolvedValue(undefined);
|
||
|
jest.spyOn(mockTar, "listTar").mockResolvedValue(undefined);
|
||
|
|
||
|
const result = await restoreCache(["/test/path"], "test-key");
|
||
|
|
||
|
expect(result).toBe("test-key"); // Should return the cache key on success
|
||
|
});
|
||
|
});
|
||
|
});
|