import { vi, describe, it, expect, beforeEach } from "vitest"; // Mock @actions/core for all tests in this file vi.mock("@actions/core", () => ({ getInput: vi.fn(), getBooleanInput: vi.fn(), info: vi.fn(), error: vi.fn(), warning: vi.fn(), setFailed: vi.fn(), setOutput: vi.fn(), })); // Mock monitor.js so postCode() integration tests don't open a real socket vi.mock("../monitor.js", () => ({ monitorConsole: vi.fn().mockResolvedValue({ sawTraceback: false, sawErrorLog: false, sawWarningLog: false, }), })); // Mock screeps-api vi.mock("screeps-api", () => { const mockApi = { auth: vi.fn().mockResolvedValue(), code: { get: vi.fn().mockResolvedValue({ ok: 1, modules: { main: "old_code" } }), set: vi.fn().mockResolvedValue({ ok: 1 }), }, }; // Use a regular function so it can be called with `new` return { ScreepsAPI: vi.fn(function () { return mockApi; }), }; }); import * as core from "@actions/core"; import { monitorConsole } from "../monitor.js"; import { validateAuthentication, replacePlaceholders, readReplaceAndWriteFiles, readFilesIntoDict, applyOnAction, postCode, } from "../index.js"; import { ScreepsAPI } from "screeps-api"; import fs from "fs"; import path from "path"; import os from "os"; import { glob } from "glob"; describe("validateAuthentication", () => { it("should return null when only token is provided", () => { expect(validateAuthentication("token", null, null)).toBeNull(); }); it("should return an error message when token and username are provided", () => { expect(validateAuthentication("token", "user", null)).toBe( "Token is defined along with username and/or password.", ); }); it("should return an error message when token and password are provided", () => { expect(validateAuthentication("token", null, "pass")).toBe( "Token is defined along with username and/or password.", ); }); it("should return an error message when token, username, and password are provided", () => { expect(validateAuthentication("token", "user", "pass")).toBe( "Token is defined along with username and/or password.", ); }); it("should return an error message when no credentials are provided", () => { expect(validateAuthentication(null, null, null)).toBe( "Neither token nor password and username are defined.", ); }); it("should return an error message when only username is provided", () => { expect(validateAuthentication(null, "user", null)).toBe( "Username is defined but no password is provided.", ); }); it("should return an error message when only password is provided", () => { expect(validateAuthentication(null, null, "pass")).toBe( "Password is defined but no username is provided.", ); }); it("should return null when username and password are provided", () => { expect(validateAuthentication(null, "user", "pass")).toBeNull(); }); }); describe("replacePlaceholders", () => { beforeEach(() => { process.env.GITHUB_SHA = "test-sha"; process.env.GITHUB_REF = "test-ref"; }); it("should replace all placeholders", () => { const content = "hash: {{gitHash}}, ref: {{gitRef}}, time: {{deployTime}}, host: {{hostname}}"; const replacedContent = replacePlaceholders(content, "test-host"); expect(replacedContent).toMatch(/hash: test-sha/); expect(replacedContent).toMatch(/ref: test-ref/); expect(replacedContent).toMatch(/time: .*/); expect(replacedContent).toMatch(/host: test-host/); }); }); describe("readReplaceAndWriteFiles", () => { let tempDir; beforeEach(async () => { tempDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), "replace-test-"), ); process.env.GITHUB_SHA = "test-sha"; process.env.GITHUB_REF = "test-ref"; }); afterEach(async () => { if (tempDir) { await fs.promises.rm(tempDir, { recursive: true, force: true }); } }); it("should find files and replace placeholders", async () => { const fileName = "test.js"; const filePath = path.join(tempDir, fileName); const content = "hash: {{gitHash}}, ref: {{gitRef}}, host: {{hostname}}"; await fs.promises.writeFile(filePath, content); const pattern = "*.js"; // We pass tempDir as the prefix so glob searches inside it await readReplaceAndWriteFiles(pattern, tempDir, "test-host"); const updatedContent = await fs.promises.readFile(filePath, "utf8"); expect(updatedContent).toContain("hash: test-sha"); expect(updatedContent).toContain("ref: test-ref"); expect(updatedContent).toContain("host: test-host"); }); }); describe("readFilesIntoDict", () => { let tempDir; beforeEach(async () => { tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "read-test-")); await fs.promises.mkdir(path.join(tempDir, "subdir"), { recursive: true }); }); afterEach(async () => { if (tempDir) { await fs.promises.rm(tempDir, { recursive: true, force: true }); } }); it("should read files into a dictionary with correct keys", async () => { const file1 = "file1.js"; const content1 = "content1"; await fs.promises.writeFile(path.join(tempDir, file1), content1); const file2 = "subdir/file2.js"; const content2 = "content2"; await fs.promises.writeFile(path.join(tempDir, file2), content2); const pattern = "**/*.js"; const result = await readFilesIntoDict(pattern, tempDir); // Keys should be relative paths without extension // On Windows, the path separator might differ, so we should be careful or just check contents // Based on implementation: // key = key.slice(prefix.length); // key = path.basename(key, path.extname(key)); // Drop the file suffix -> THIS IS BUGGY for subdirs? // Let's check the implementation of readFilesIntoDict again in index.js // It does: key = path.basename(key, path.extname(key)); // This removes the directory part! So subdir/file2.js becomes file2 expect(result["file1"]).toBe(content1); expect(result["file2"]).toBe(content2); }); }); describe("glob functionality", () => { let tempDir; beforeEach(async () => { tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "glob-test-")); await fs.promises.mkdir(path.join(tempDir, "lib"), { recursive: true }); await fs.promises.mkdir(path.join(tempDir, "deep", "folder"), { recursive: true, }); await fs.promises.writeFile(path.join(tempDir, "main.js"), "content"); await fs.promises.writeFile(path.join(tempDir, "utils.js"), "content"); await fs.promises.writeFile( path.join(tempDir, "lib", "helper.js"), "content", ); await fs.promises.writeFile( path.join(tempDir, "lib", "data.json"), "content", ); await fs.promises.writeFile( path.join(tempDir, "deep", "folder", "main.js"), "content", ); }); afterEach(async () => { if (tempDir) { await fs.promises.rm(tempDir, { recursive: true, force: true }); } }); it("should find all javascript files in the directory", async () => { // Ensure pattern uses forward slashes for glob const pattern = path.join(tempDir, "**", "*.js").split(path.sep).join("/"); const files = await glob(pattern); // Normalize file paths to system separator (backslashes on Windows) const normalizedFiles = files.map((f) => path.normalize(f)); const expectedFiles = [ path.join(tempDir, "deep", "folder", "main.js"), path.join(tempDir, "lib", "helper.js"), path.join(tempDir, "main.js"), path.join(tempDir, "utils.js"), ].sort(); expect(normalizedFiles.sort()).toEqual(expectedFiles); }); }); // ──────────────────────────────────────────────────────────────────────────── // applyOnAction // ──────────────────────────────────────────────────────────────────────────── describe("applyOnAction", () => { beforeEach(() => vi.clearAllMocks()); it("'ignore' + true → no core call, returns false", () => { expect(applyOnAction("ignore", true, "msg")).toBe(false); expect(core.warning).not.toHaveBeenCalled(); expect(core.setFailed).not.toHaveBeenCalled(); }); it("'warn' + true → core.warning() called with message, returns false", () => { expect(applyOnAction("warn", true, "boom")).toBe(false); expect(core.warning).toHaveBeenCalledWith("boom"); expect(core.setFailed).not.toHaveBeenCalled(); }); it("'fail' + true → core.setFailed() called with message, returns true", () => { expect(applyOnAction("fail", true, "boom")).toBe(true); expect(core.setFailed).toHaveBeenCalledWith("boom"); expect(core.warning).not.toHaveBeenCalled(); }); it("'fail' + false → no core call, returns false", () => { expect(applyOnAction("fail", false, "boom")).toBe(false); expect(core.setFailed).not.toHaveBeenCalled(); }); it("'warn' + false → no core call, returns false", () => { expect(applyOnAction("warn", false, "msg")).toBe(false); expect(core.warning).not.toHaveBeenCalled(); }); }); // ──────────────────────────────────────────────────────────────────────────── // postCode — monitor wiring // ──────────────────────────────────────────────────────────────────────────── describe("postCode — monitor wiring", () => { beforeEach(() => { vi.clearAllMocks(); // Default core mocks core.getInput.mockImplementation((name) => { if (name === "monitor") return "0"; if (name === "token") return "test-token"; if (name === "branch") return "default"; if (name === "on_traceback") return "fail"; return ""; }); core.getBooleanInput.mockImplementation((name) => { if (name === "rollback_on_failure") return false; return false; }); }); it("does not call monitorConsole when monitor=0 (default)", async () => { // We just run postCode with monitor=0 and verify monitorConsole is not called. await postCode(); expect(monitorConsole).not.toHaveBeenCalled(); }); it("rolls back to previous code when monitor detects a failure and rollback_on_failure is true", async () => { // Setup inputs for monitor and rollback core.getInput.mockImplementation((name) => { if (name === "monitor") return "10"; if (name === "token") return "test-token"; if (name === "branch") return "default"; if (name === "on_traceback") return "fail"; return ""; }); core.getBooleanInput.mockImplementation((name) => { if (name === "rollback_on_failure") return true; return false; }); // Simulate a failure in monitorConsole monitorConsole.mockResolvedValueOnce({ sawTraceback: true, // Should trigger "fail" due to on_traceback=fail sawErrorLog: false, sawWarningLog: false, }); await postCode(); // Verify rollback was performed const mockApiInstance = new ScreepsAPI(); // `code.set` should be called twice: // 1st time: uploading the new files // 2nd time: rolling back to oldCode expect(mockApiInstance.code.set).toHaveBeenCalledTimes(2); expect(mockApiInstance.code.set).toHaveBeenNthCalledWith(2, "default", { main: "old_code", }); // Verify it called core.setFailed due to traceback expect(core.setFailed).toHaveBeenCalledWith( "Screeps console: traceback detected", ); }); });