mirror of https://github.com/actions/cache.git
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			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
 | |
|         });
 | |
|     });
 | |
| });
 |