From c2b0d3157858167490afa323c9356c88c1016c1d Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sun, 17 May 2026 01:15:52 -0500 Subject: [PATCH] 438 tests per windows builds now --- Overlord-Server/src/build-rate-limit.test.ts | 115 ++++++++++++++++++ .../src/server/build-utils.test.ts | 93 ++++++++++++++ Overlord-Server/src/server/http-utils.test.ts | 79 ++++++++++++ .../src/server/plugin-utils.test.ts | 26 ++++ 4 files changed, 313 insertions(+) create mode 100644 Overlord-Server/src/build-rate-limit.test.ts create mode 100644 Overlord-Server/src/server/build-utils.test.ts create mode 100644 Overlord-Server/src/server/http-utils.test.ts create mode 100644 Overlord-Server/src/server/plugin-utils.test.ts diff --git a/Overlord-Server/src/build-rate-limit.test.ts b/Overlord-Server/src/build-rate-limit.test.ts new file mode 100644 index 0000000..645036e --- /dev/null +++ b/Overlord-Server/src/build-rate-limit.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, test } from "bun:test"; +import { + canUserBuild, + getBuildRateLimitStats, + recordBuildEnd, + recordBuildStart, +} from "./build-rate-limit"; + +// Module state is process-global. Each test uses a unique userId so concurrent +// or sequential tests don't leak state into each other, and always pairs every +// recordBuildStart with a recordBuildEnd in a try/finally to keep global counts +// clean for unrelated tests. + +let nextUserId = 1_000_000; +function uniqueUser(): number { + nextUserId += 1; + return nextUserId; +} + +describe("canUserBuild", () => { + test("allows a fresh user", () => { + expect(canUserBuild(uniqueUser())).toEqual({ allowed: true }); + }); + + test("blocks user once their per-user concurrent limit is reached", () => { + const userId = uniqueUser(); + const started: number[] = []; + try { + // Saturate the per-user concurrent slot (default config: 1). + while (true) { + const decision = canUserBuild(userId); + if (!decision.allowed) break; + recordBuildStart(userId); + started.push(userId); + if (started.length > 20) throw new Error("unexpected: never hit per-user cap"); + } + const blocked = canUserBuild(userId); + expect(blocked.allowed).toBe(false); + expect(blocked.reason).toMatch(/concurrent per user/i); + } finally { + for (const u of started) recordBuildEnd(u); + } + }); + + test("releases the slot after recordBuildEnd", () => { + const userId = uniqueUser(); + recordBuildStart(userId); + try { + const mid = canUserBuild(userId); + // May be blocked depending on per-user cap; either way recordBuildEnd must restore capacity. + if (!mid.allowed) { + recordBuildEnd(userId); + expect(canUserBuild(userId).allowed).toBe(true); + // Re-start so the finally block's recordBuildEnd matches a started build. + recordBuildStart(userId); + } + } finally { + recordBuildEnd(userId); + } + }); +}); + +describe("recordBuildStart / recordBuildEnd", () => { + test("globalActive increases then decreases symmetrically", () => { + const userId = uniqueUser(); + const before = getBuildRateLimitStats().globalActive; + recordBuildStart(userId); + try { + expect(getBuildRateLimitStats().globalActive).toBe(before + 1); + } finally { + recordBuildEnd(userId); + } + expect(getBuildRateLimitStats().globalActive).toBe(before); + }); + + test("recordBuildEnd on a user with no active builds does not underflow", () => { + const userId = uniqueUser(); + const before = getBuildRateLimitStats().globalActive; + // No matching start. Should be a no-op for global count. + recordBuildEnd(userId); + expect(getBuildRateLimitStats().globalActive).toBe(before); + }); + + test("multiple starts accumulate active count per user", () => { + const userId = uniqueUser(); + recordBuildStart(userId); + recordBuildStart(userId); + try { + const stats = getBuildRateLimitStats(); + const entry = stats.perUser.find((u) => u.userId === userId); + expect(entry).toBeDefined(); + expect(entry!.active).toBe(2); + expect(entry!.recentCount).toBe(2); + } finally { + recordBuildEnd(userId); + recordBuildEnd(userId); + } + }); +}); + +describe("getBuildRateLimitStats", () => { + test("omits users with no recent or active builds", () => { + const userId = uniqueUser(); + recordBuildStart(userId); + recordBuildEnd(userId); + // Without waiting an hour the timestamp is still 'recent', so the entry stays — + // but stats should still report it as active=0 if anything is reported at all. + const stats = getBuildRateLimitStats(); + const entry = stats.perUser.find((u) => u.userId === userId); + if (entry) { + expect(entry.active).toBe(0); + expect(entry.recentCount).toBeGreaterThan(0); + } + }); +}); diff --git a/Overlord-Server/src/server/build-utils.test.ts b/Overlord-Server/src/server/build-utils.test.ts new file mode 100644 index 0000000..0784d4c --- /dev/null +++ b/Overlord-Server/src/server/build-utils.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, test } from "bun:test"; +import { generateBuildMutex, sanitizeMutex, sanitizeOutputName } from "./build-utils"; + +describe("generateBuildMutex", () => { + test("returns string of requested length", () => { + expect(generateBuildMutex(24)).toHaveLength(24); + expect(generateBuildMutex(1)).toHaveLength(1); + expect(generateBuildMutex(64)).toHaveLength(64); + }); + + test("defaults to length 24", () => { + expect(generateBuildMutex()).toHaveLength(24); + }); + + test("only uses allowed alphabet characters", () => { + const value = generateBuildMutex(256); + expect(value).toMatch(/^[A-Za-z0-9._-]+$/); + }); + + test("produces different values across calls", () => { + const a = generateBuildMutex(24); + const b = generateBuildMutex(24); + expect(a).not.toBe(b); + }); + + test("result passes sanitizeMutex", () => { + expect(sanitizeMutex(generateBuildMutex(24))).toBeDefined(); + }); +}); + +describe("sanitizeMutex", () => { + test("returns undefined for falsy input", () => { + expect(sanitizeMutex(undefined)).toBeUndefined(); + expect(sanitizeMutex("")).toBeUndefined(); + }); + + test("returns undefined for whitespace-only input", () => { + expect(sanitizeMutex(" ")).toBeUndefined(); + expect(sanitizeMutex("\t\n")).toBeUndefined(); + }); + + test("trims surrounding whitespace", () => { + expect(sanitizeMutex(" abc ")).toBe("abc"); + }); + + test("accepts allowed characters", () => { + expect(sanitizeMutex("Abc.123_-xyz")).toBe("Abc.123_-xyz"); + }); + + test("rejects spaces inside the value", () => { + expect(() => sanitizeMutex("ab cd")).toThrow(/Mutex must be/); + }); + + test("rejects disallowed punctuation", () => { + expect(() => sanitizeMutex("abc/def")).toThrow(); + expect(() => sanitizeMutex("abc@def")).toThrow(); + expect(() => sanitizeMutex("abc#def")).toThrow(); + }); + + test("enforces max length of 64", () => { + expect(sanitizeMutex("a".repeat(64))).toBe("a".repeat(64)); + expect(() => sanitizeMutex("a".repeat(65))).toThrow(); + }); +}); + +describe("sanitizeOutputName", () => { + test("accepts plain filenames", () => { + expect(sanitizeOutputName("agent.exe")).toBe("agent.exe"); + expect(sanitizeOutputName("my-build_v2.0.bin")).toBe("my-build_v2.0.bin"); + }); + + test("strips path components from traversal attempts", () => { + // path.basename removes the directory portion, neutralizing traversal. + // The returned value still has to match the allowed-character set. + expect(sanitizeOutputName("../etc/passwd")).toBe("passwd"); + expect(sanitizeOutputName("/etc/shadow")).toBe("shadow"); + }); + + test("rejects when basename itself contains a disallowed character", () => { + expect(() => sanitizeOutputName("bad name")).toThrow(/Invalid output filename/); + expect(() => sanitizeOutputName("weird$.bin")).toThrow(); + }); + + test("rejects names with disallowed characters", () => { + expect(() => sanitizeOutputName("agent name.exe")).toThrow(); + expect(() => sanitizeOutputName("agent$.exe")).toThrow(); + expect(() => sanitizeOutputName("agent*.exe")).toThrow(); + }); + + test("rejects empty result", () => { + expect(() => sanitizeOutputName("")).toThrow(); + }); +}); diff --git a/Overlord-Server/src/server/http-utils.test.ts b/Overlord-Server/src/server/http-utils.test.ts new file mode 100644 index 0000000..4bb11a5 --- /dev/null +++ b/Overlord-Server/src/server/http-utils.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, test } from "bun:test"; +import { mimeType, secureHeaders } from "./http-utils"; +import { SECURITY_HEADERS } from "./http-security"; + +describe("mimeType", () => { + test("maps common web extensions", () => { + expect(mimeType("index.html")).toBe("text/html; charset=utf-8"); + expect(mimeType("app.css")).toBe("text/css; charset=utf-8"); + expect(mimeType("bundle.js")).toBe("text/javascript; charset=utf-8"); + expect(mimeType("worker.mjs")).toBe("text/javascript; charset=utf-8"); + expect(mimeType("data.json")).toBe("application/json"); + }); + + test("maps image extensions", () => { + expect(mimeType("a.png")).toBe("image/png"); + expect(mimeType("a.jpg")).toBe("image/jpeg"); + expect(mimeType("a.jpeg")).toBe("image/jpeg"); + expect(mimeType("a.webp")).toBe("image/webp"); + expect(mimeType("a.svg")).toBe("image/svg+xml"); + expect(mimeType("a.ico")).toBe("image/x-icon"); + }); + + test("maps font extensions", () => { + expect(mimeType("Inter.woff")).toBe("font/woff"); + expect(mimeType("Inter.woff2")).toBe("font/woff2"); + expect(mimeType("Inter.ttf")).toBe("font/ttf"); + }); + + test("is case insensitive on the extension", () => { + expect(mimeType("PIC.PNG")).toBe("image/png"); + expect(mimeType("Style.CSS")).toBe("text/css; charset=utf-8"); + }); + + test("uses only the final extension", () => { + expect(mimeType("archive.tar.gz")).toBe("application/octet-stream"); + expect(mimeType("script.min.js")).toBe("text/javascript; charset=utf-8"); + }); + + test("returns octet-stream for unknown extensions", () => { + expect(mimeType("file.xyz")).toBe("application/octet-stream"); + expect(mimeType("binary.bin")).toBe("application/octet-stream"); + }); + + test("returns octet-stream when there is no extension", () => { + expect(mimeType("README")).toBe("application/octet-stream"); + expect(mimeType("")).toBe("application/octet-stream"); + }); + + test("handles full paths", () => { + expect(mimeType("/var/www/index.html")).toBe("text/html; charset=utf-8"); + expect(mimeType("C:\\dist\\app.js")).toBe("text/javascript; charset=utf-8"); + }); +}); + +describe("secureHeaders", () => { + test("includes every security header by default", () => { + const headers = secureHeaders(); + for (const [name, value] of Object.entries(SECURITY_HEADERS)) { + expect(headers[name as keyof typeof headers]).toBe(value); + } + }); + + test("omits Content-Type when none provided", () => { + const headers = secureHeaders(); + expect((headers as Record)["Content-Type"]).toBeUndefined(); + }); + + test("adds Content-Type when provided", () => { + const headers = secureHeaders("application/json"); + expect((headers as Record)["Content-Type"]).toBe("application/json"); + }); + + test("returns a fresh object each call (no shared mutation)", () => { + const a = secureHeaders("text/plain") as Record; + const b = secureHeaders("text/html") as Record; + a["Content-Type"] = "mutated"; + expect(b["Content-Type"]).toBe("text/html"); + }); +}); diff --git a/Overlord-Server/src/server/plugin-utils.test.ts b/Overlord-Server/src/server/plugin-utils.test.ts new file mode 100644 index 0000000..382aa97 --- /dev/null +++ b/Overlord-Server/src/server/plugin-utils.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "bun:test"; +import { sanitizePluginId } from "./plugin-utils"; + +describe("sanitizePluginId", () => { + test("accepts a plain id", () => { + expect(sanitizePluginId("my-plugin")).toBe("my-plugin"); + expect(sanitizePluginId("plugin.v2_0")).toBe("plugin.v2_0"); + }); + + test("strips path components to defend against traversal", () => { + expect(sanitizePluginId("../../../etc/passwd")).toBe("passwd"); + expect(sanitizePluginId("/var/log/syslog")).toBe("syslog"); + expect(sanitizePluginId("nested/dir/plugin")).toBe("plugin"); + }); + + test("strips disallowed characters from the basename", () => { + expect(sanitizePluginId("hello world!")).toBe("helloworld"); + expect(sanitizePluginId("plug$in@123")).toBe("plugin123"); + }); + + test("throws when nothing survives sanitization", () => { + expect(() => sanitizePluginId("")).toThrow(/Invalid plugin id/); + expect(() => sanitizePluginId("!!!@@@")).toThrow(/Invalid plugin id/); + expect(() => sanitizePluginId("/")).toThrow(/Invalid plugin id/); + }); +});