From de0f54b54acc8b2b394f4c18bbbba431f23fab7c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Apr 2026 12:36:24 +0100 Subject: [PATCH] test(security): isolate windows acl user fallback --- src/security/windows-acl.test.ts | 33 ++++++-------------------------- src/security/windows-acl.ts | 24 +++++++++++++++++------ 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/src/security/windows-acl.test.ts b/src/security/windows-acl.test.ts index b1786149f1f..5e530327375 100644 --- a/src/security/windows-acl.test.ts +++ b/src/security/windows-acl.test.ts @@ -2,24 +2,8 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { WindowsAclEntry, WindowsAclSummary } from "./windows-acl.js"; const MOCK_USERNAME = "MockUser"; -const userInfoMock = vi.hoisted(() => - vi.fn(() => ({ - username: MOCK_USERNAME, - uid: -1, - gid: -1, - shell: "C:\\Windows\\System32\\cmd.exe", - homedir: "C:\\Users\\MockUser", - })), -); - -vi.mock("node:os", async () => { - const { mockNodeBuiltinModule } = await import("openclaw/plugin-sdk/test-node-mocks"); - return mockNodeBuiltinModule( - () => vi.importActual("node:os"), - { userInfo: userInfoMock as unknown as typeof import("node:os").userInfo }, - { mirrorToDefault: true }, - ); -}); +const mockUserInfo = () => ({ username: MOCK_USERNAME }); +const emptyUserInfo = () => ({ username: "" }); let createIcaclsResetCommand: typeof import("./windows-acl.js").createIcaclsResetCommand; let formatIcaclsResetCommand: typeof import("./windows-acl.js").formatIcaclsResetCommand; @@ -125,7 +109,7 @@ describe("windows-acl", () => { it("falls back to os.userInfo when USERNAME is empty", () => { // When USERNAME env is empty, falls back to os.userInfo().username const env = { USERNAME: "", USERDOMAIN: "WORKGROUP" }; - const result = resolveWindowsUserPrincipal(env); + const result = resolveWindowsUserPrincipal(env, mockUserInfo); // Should return a username (from os.userInfo fallback) with WORKGROUP domain expect(result).toBe(`WORKGROUP\\${MOCK_USERNAME}`); }); @@ -653,6 +637,7 @@ Successfully processed 1 files`; const result = formatIcaclsResetCommand("C:\\test\\file.txt", { isDir: false, env: {}, + userInfo: mockUserInfo, }); // Should contain the actual system username from os.userInfo expect(result).toContain(`"${MOCK_USERNAME}:F"`); @@ -678,6 +663,7 @@ Successfully processed 1 files`; const result = createIcaclsResetCommand("C:\\test\\file.txt", { isDir: false, env: {}, + userInfo: mockUserInfo, }); // Should return a valid command using the system username expect(result).not.toBeNull(); @@ -717,17 +703,10 @@ Successfully processed 1 files`; }); it("returns null when no username can be resolved (line 348)", () => { - // Temporarily make os.userInfo().username empty so resolveWindowsUserPrincipal returns null - userInfoMock.mockReturnValueOnce({ - username: "", - uid: -1, - gid: -1, - shell: "", - homedir: "", - }); const result = createIcaclsResetCommand("C:\\test\\file.txt", { isDir: false, env: { USERNAME: "", USERDOMAIN: "" }, + userInfo: emptyUserInfo, }); expect(result).toBeNull(); }); diff --git a/src/security/windows-acl.ts b/src/security/windows-acl.ts index bac804feaf3..8c29bada485 100644 --- a/src/security/windows-acl.ts +++ b/src/security/windows-acl.ts @@ -22,6 +22,14 @@ export type WindowsAclSummary = { error?: string; }; +export type WindowsUserInfoProvider = () => { username?: string | null }; + +export type IcaclsResetCommandOptions = { + isDir: boolean; + env?: NodeJS.ProcessEnv; + userInfo?: WindowsUserInfoProvider; +}; + const INHERIT_FLAGS = new Set(["I", "OI", "CI", "IO", "NP"]); const WORLD_PRINCIPALS = new Set([ "everyone", @@ -66,14 +74,18 @@ const STATUS_PREFIXES = [ ]; const normalize = (value: string) => normalizeLowercaseStringOrEmpty(value); +const defaultWindowsUserInfo: WindowsUserInfoProvider = () => os.userInfo(); function normalizeSid(value: string): string { const normalized = normalize(value); return normalized.startsWith("*") ? normalized.slice(1) : normalized; } -export function resolveWindowsUserPrincipal(env?: NodeJS.ProcessEnv): string | null { - const username = env?.USERNAME?.trim() || os.userInfo().username?.trim(); +export function resolveWindowsUserPrincipal( + env?: NodeJS.ProcessEnv, + userInfo: WindowsUserInfoProvider = defaultWindowsUserInfo, +): string | null { + const username = env?.USERNAME?.trim() || userInfo().username?.trim(); if (!username) { return null; } @@ -361,18 +373,18 @@ export function formatWindowsAclSummary(summary: WindowsAclSummary): string { export function formatIcaclsResetCommand( targetPath: string, - opts: { isDir: boolean; env?: NodeJS.ProcessEnv }, + opts: IcaclsResetCommandOptions, ): string { - const user = resolveWindowsUserPrincipal(opts.env) ?? "%USERNAME%"; + const user = resolveWindowsUserPrincipal(opts.env, opts.userInfo) ?? "%USERNAME%"; const grant = opts.isDir ? "(OI)(CI)F" : "F"; return `icacls "${targetPath}" /inheritance:r /grant:r "${user}:${grant}" /grant:r "*S-1-5-18:${grant}"`; } export function createIcaclsResetCommand( targetPath: string, - opts: { isDir: boolean; env?: NodeJS.ProcessEnv }, + opts: IcaclsResetCommandOptions, ): { command: string; args: string[]; display: string } | null { - const user = resolveWindowsUserPrincipal(opts.env); + const user = resolveWindowsUserPrincipal(opts.env, opts.userInfo); if (!user) { return null; }