From 837cec64af8a7dfc989c5417b21a11cd55e7ac7f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 9 Jan 2026 18:47:52 +0100 Subject: [PATCH] refactor: centralize test path containment checks --- src/media/store.test.ts | 43 ++++++++++++++++++----------------------- src/web/logout.test.ts | 22 ++------------------- test/helpers/paths.ts | 19 ++++++++++++++++++ 3 files changed, 40 insertions(+), 44 deletions(-) create mode 100644 test/helpers/paths.ts diff --git a/src/media/store.test.ts b/src/media/store.test.ts index 8d8ed036b..2a797e886 100644 --- a/src/media/store.test.ts +++ b/src/media/store.test.ts @@ -4,27 +4,34 @@ import JSZip from "jszip"; import sharp from "sharp"; import { describe, expect, it, vi } from "vitest"; +import { isPathWithinBase } from "../../test/helpers/paths.js"; import { withTempHome } from "../../test/helpers/temp-home.js"; describe("media store", () => { - it("creates and returns media directory", async () => { - await withTempHome(async () => { + async function withTempStore( + fn: (store: typeof import("./store.js"), home: string) => Promise, + ): Promise { + return await withTempHome(async (home) => { vi.resetModules(); const store = await import("./store.js"); + return await fn(store, home); + }); + } + it("creates and returns media directory", async () => { + await withTempStore(async (store, home) => { const dir = await store.ensureMediaDir(); - const normalized = path.normalize(dir); - expect(normalized).toContain(`${path.sep}.clawdbot${path.sep}media`); + expect(isPathWithinBase(home, dir)).toBe(true); + expect(path.normalize(dir)).toContain( + `${path.sep}.clawdbot${path.sep}media`, + ); const stat = await fs.stat(dir); expect(stat.isDirectory()).toBe(true); }); }); it("saves buffers and enforces size limit", async () => { - await withTempHome(async () => { - vi.resetModules(); - const store = await import("./store.js"); - + await withTempStore(async (store) => { const buf = Buffer.from("hello"); const saved = await store.saveMediaBuffer(buf, "text/plain"); const savedStat = await fs.stat(saved.path); @@ -49,10 +56,7 @@ describe("media store", () => { }); it("copies local files and cleans old media", async () => { - await withTempHome(async (home) => { - vi.resetModules(); - const store = await import("./store.js"); - + await withTempStore(async (store, home) => { const srcFile = path.join(home, "tmp-src.txt"); await fs.mkdir(home, { recursive: true }); await fs.writeFile(srcFile, "local file"); @@ -71,10 +75,7 @@ describe("media store", () => { }); it("sets correct mime for xlsx by extension", async () => { - await withTempHome(async (home) => { - vi.resetModules(); - const store = await import("./store.js"); - + await withTempStore(async (store, home) => { const xlsxPath = path.join(home, "sheet.xlsx"); await fs.mkdir(home, { recursive: true }); await fs.writeFile(xlsxPath, "not really an xlsx"); @@ -88,10 +89,7 @@ describe("media store", () => { }); it("renames media based on detected mime even when extension is wrong", async () => { - await withTempHome(async (home) => { - vi.resetModules(); - const store = await import("./store.js"); - + await withTempStore(async (store, home) => { const pngBytes = await sharp({ create: { width: 2, height: 2, channels: 3, background: "#00ff00" }, }) @@ -110,10 +108,7 @@ describe("media store", () => { }); it("sniffs xlsx mime for zip buffers and renames extension", async () => { - await withTempHome(async (home) => { - vi.resetModules(); - const store = await import("./store.js"); - + await withTempStore(async (store, home) => { const zip = new JSZip(); zip.file( "[Content_Types].xml", diff --git a/src/web/logout.test.ts b/src/web/logout.test.ts index bc27e8c38..715b92896 100644 --- a/src/web/logout.test.ts +++ b/src/web/logout.test.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { isPathWithinBase } from "../../test/helpers/paths.js"; import { withTempHome } from "../../test/helpers/temp-home.js"; const runtime = { @@ -28,26 +29,7 @@ describe("web logout", () => { vi.resetModules(); const { logoutWeb, WA_WEB_AUTH_DIR } = await import("./session.js"); - if (process.platform === "win32") { - const normalizedHome = path.win32.normalize(home).toLowerCase(); - const normalizedAuthDir = path.win32 - .normalize(WA_WEB_AUTH_DIR) - .toLowerCase(); - const rel = path.win32.relative(normalizedHome, normalizedAuthDir); - const isWithinHome = - rel.length > 0 && - !rel.startsWith("..") && - !path.win32.isAbsolute(rel); - expect(isWithinHome).toBe(true); - } else { - const rel = path.relative( - path.resolve(home), - path.resolve(WA_WEB_AUTH_DIR), - ); - expect(rel && !rel.startsWith("..") && !path.isAbsolute(rel)).toBe( - true, - ); - } + expect(isPathWithinBase(home, WA_WEB_AUTH_DIR)).toBe(true); fs.mkdirSync(WA_WEB_AUTH_DIR, { recursive: true }); fs.writeFileSync(path.join(WA_WEB_AUTH_DIR, "creds.json"), "{}"); diff --git a/test/helpers/paths.ts b/test/helpers/paths.ts new file mode 100644 index 000000000..25bcf3324 --- /dev/null +++ b/test/helpers/paths.ts @@ -0,0 +1,19 @@ +import path from "node:path"; + +export function isPathWithinBase(base: string, target: string): boolean { + if (process.platform === "win32") { + const normalizedBase = path.win32.normalize(path.win32.resolve(base)); + const normalizedTarget = path.win32.normalize(path.win32.resolve(target)); + + const rel = path.win32.relative( + normalizedBase.toLowerCase(), + normalizedTarget.toLowerCase(), + ); + return rel === "" || (!rel.startsWith("..") && !path.win32.isAbsolute(rel)); + } + + const normalizedBase = path.resolve(base); + const normalizedTarget = path.resolve(target); + const rel = path.relative(normalizedBase, normalizedTarget); + return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel)); +}