diff --git a/src/shared/avatar-policy.test.ts b/src/shared/avatar-policy.test.ts index 81331a45b8d..cbc345767e7 100644 --- a/src/shared/avatar-policy.test.ts +++ b/src/shared/avatar-policy.test.ts @@ -1,24 +1,42 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { + hasAvatarUriScheme, + isAvatarDataUrl, + isAvatarHttpUrl, + isAvatarImageDataUrl, isPathWithinRoot, isSupportedLocalAvatarExtension, + isWindowsAbsolutePath, isWorkspaceRelativeAvatarPath, looksLikeAvatarPath, resolveAvatarMime, } from "./avatar-policy.js"; describe("avatar policy", () => { + it("classifies avatar URI and path helpers directly", () => { + expect(isAvatarDataUrl("data:text/plain,hello")).toBe(true); + expect(isAvatarImageDataUrl("data:image/png;base64,AAAA")).toBe(true); + expect(isAvatarImageDataUrl("data:text/plain,hello")).toBe(false); + expect(isAvatarHttpUrl("https://example.com/avatar.png")).toBe(true); + expect(isAvatarHttpUrl("ftp://example.com/avatar.png")).toBe(false); + expect(hasAvatarUriScheme("slack://avatar")).toBe(true); + expect(isWindowsAbsolutePath("C:\\\\avatars\\\\openclaw.png")).toBe(true); + }); + it("accepts workspace-relative avatar paths and rejects URI schemes", () => { expect(isWorkspaceRelativeAvatarPath("avatars/openclaw.png")).toBe(true); expect(isWorkspaceRelativeAvatarPath("C:\\\\avatars\\\\openclaw.png")).toBe(true); expect(isWorkspaceRelativeAvatarPath("https://example.com/avatar.png")).toBe(false); expect(isWorkspaceRelativeAvatarPath("data:image/png;base64,AAAA")).toBe(false); expect(isWorkspaceRelativeAvatarPath("~/avatar.png")).toBe(false); + expect(isWorkspaceRelativeAvatarPath("slack://avatar")).toBe(false); + expect(isWorkspaceRelativeAvatarPath("")).toBe(false); }); it("checks path containment safely", () => { const root = path.resolve("/tmp/root"); + expect(isPathWithinRoot(root, root)).toBe(true); expect(isPathWithinRoot(root, path.resolve("/tmp/root/avatars/a.png"))).toBe(true); expect(isPathWithinRoot(root, path.resolve("/tmp/root/../outside.png"))).toBe(false); }); @@ -38,6 +56,7 @@ describe("avatar policy", () => { it("resolves mime type from extension", () => { expect(resolveAvatarMime("a.svg")).toBe("image/svg+xml"); expect(resolveAvatarMime("a.tiff")).toBe("image/tiff"); + expect(resolveAvatarMime("A.PNG")).toBe("image/png"); expect(resolveAvatarMime("a.bin")).toBe("application/octet-stream"); }); }); diff --git a/src/shared/operator-scope-compat.test.ts b/src/shared/operator-scope-compat.test.ts index 11810673681..e48a17ad398 100644 --- a/src/shared/operator-scope-compat.test.ts +++ b/src/shared/operator-scope-compat.test.ts @@ -86,4 +86,31 @@ describe("roleScopesAllow", () => { }), ).toBe(false); }); + + it("normalizes blank and duplicate scopes before evaluating", () => { + expect( + roleScopesAllow({ + role: " operator ", + requestedScopes: [" operator.read ", "operator.read", " "], + allowedScopes: [" operator.write ", "operator.write", ""], + }), + ).toBe(true); + }); + + it("rejects unsatisfied operator write scopes and empty allowed scopes", () => { + expect( + roleScopesAllow({ + role: "operator", + requestedScopes: ["operator.write"], + allowedScopes: ["operator.read"], + }), + ).toBe(false); + expect( + roleScopesAllow({ + role: "operator", + requestedScopes: ["operator.read"], + allowedScopes: [" "], + }), + ).toBe(false); + }); });