cache/__tests__/downloadValidation.test.ts

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
});
});
});