|
| 1 | +import { describe, it, expect } from "vitest" |
| 2 | +import { escapeHtml, escapeHtmlInObject, escapeClineMessage } from "../htmlEscape" |
| 3 | + |
| 4 | +describe("htmlEscape", () => { |
| 5 | + describe("escapeHtml", () => { |
| 6 | + it("should escape HTML special characters", () => { |
| 7 | + expect(escapeHtml('<script>alert("XSS")</script>')).toBe( |
| 8 | + "<script>alert("XSS")</script>", |
| 9 | + ) |
| 10 | + }) |
| 11 | + |
| 12 | + it("should escape angle brackets used in diff markers", () => { |
| 13 | + expect(escapeHtml("<<<<<<< SEARCH")).toBe("<<<<<<< SEARCH") |
| 14 | + expect(escapeHtml("=======")).toBe("=======") |
| 15 | + expect(escapeHtml(">>>>>>> REPLACE")).toBe(">>>>>>> REPLACE") |
| 16 | + }) |
| 17 | + |
| 18 | + it("should escape all special characters", () => { |
| 19 | + expect(escapeHtml("& < > \" ' /")).toBe("& < > " ' /") |
| 20 | + }) |
| 21 | + |
| 22 | + it("should handle empty strings", () => { |
| 23 | + expect(escapeHtml("")).toBe("") |
| 24 | + }) |
| 25 | + |
| 26 | + it("should handle strings without special characters", () => { |
| 27 | + expect(escapeHtml("Hello World")).toBe("Hello World") |
| 28 | + }) |
| 29 | + |
| 30 | + it("should handle the specific Windows crash case", () => { |
| 31 | + const diffContent = `<<<<<<< SEARCH |
| 32 | +:start_line:1 |
| 33 | +------- |
| 34 | +def calculate_total(items): |
| 35 | + total = 0 |
| 36 | + for item in items: |
| 37 | + total += item |
| 38 | + return total |
| 39 | +======= |
| 40 | +def calculate_total(items): |
| 41 | + """Calculate total with 10% markup""" |
| 42 | + return sum(item * 1.1 for item in items) |
| 43 | +>>>>>>> REPLACE` |
| 44 | + |
| 45 | + const escaped = escapeHtml(diffContent) |
| 46 | + expect(escaped).toContain("<<<<<<< SEARCH") |
| 47 | + expect(escaped).toContain(">>>>>>> REPLACE") |
| 48 | + expect(escaped).not.toContain("<<<<<<< SEARCH") |
| 49 | + expect(escaped).not.toContain(">>>>>>> REPLACE") |
| 50 | + }) |
| 51 | + }) |
| 52 | + |
| 53 | + describe("escapeHtmlInObject", () => { |
| 54 | + it("should escape strings in objects", () => { |
| 55 | + const obj = { |
| 56 | + text: "<div>Hello</div>", |
| 57 | + nested: { |
| 58 | + value: '<script>alert("XSS")</script>', |
| 59 | + }, |
| 60 | + } |
| 61 | + |
| 62 | + const escaped = escapeHtmlInObject(obj) |
| 63 | + expect(escaped.text).toBe("<div>Hello</div>") |
| 64 | + expect(escaped.nested.value).toBe("<script>alert("XSS")</script>") |
| 65 | + }) |
| 66 | + |
| 67 | + it("should handle arrays", () => { |
| 68 | + const arr = ["<div>1</div>", "<div>2</div>"] |
| 69 | + const escaped = escapeHtmlInObject(arr) |
| 70 | + expect(escaped[0]).toBe("<div>1</div>") |
| 71 | + expect(escaped[1]).toBe("<div>2</div>") |
| 72 | + }) |
| 73 | + |
| 74 | + it("should handle null and undefined", () => { |
| 75 | + expect(escapeHtmlInObject(null)).toBe(null) |
| 76 | + expect(escapeHtmlInObject(undefined)).toBe(undefined) |
| 77 | + }) |
| 78 | + |
| 79 | + it("should preserve non-string values", () => { |
| 80 | + const obj = { |
| 81 | + text: "<div>Hello</div>", |
| 82 | + number: 123, |
| 83 | + boolean: true, |
| 84 | + date: new Date("2024-01-01"), |
| 85 | + } |
| 86 | + |
| 87 | + const escaped = escapeHtmlInObject(obj) |
| 88 | + expect(escaped.text).toBe("<div>Hello</div>") |
| 89 | + expect(escaped.number).toBe(123) |
| 90 | + expect(escaped.boolean).toBe(true) |
| 91 | + expect(escaped.date).toEqual(new Date("2024-01-01")) |
| 92 | + }) |
| 93 | + }) |
| 94 | + |
| 95 | + describe("escapeClineMessage", () => { |
| 96 | + it("should escape text field in ClineMessage", () => { |
| 97 | + const message = { |
| 98 | + ts: 1234567890, |
| 99 | + type: "say", |
| 100 | + say: "tool", |
| 101 | + text: "<<<<<<< SEARCH\n:start_line:1\n-------\ncode here\n=======\nnew code\n>>>>>>> REPLACE", |
| 102 | + } |
| 103 | + |
| 104 | + const escaped = escapeClineMessage(message) |
| 105 | + expect(escaped.text).toContain("<<<<<<< SEARCH") |
| 106 | + expect(escaped.text).toContain(">>>>>>> REPLACE") |
| 107 | + expect(escaped.ts).toBe(1234567890) |
| 108 | + expect(escaped.type).toBe("say") |
| 109 | + }) |
| 110 | + |
| 111 | + it("should escape reasoning field if present", () => { |
| 112 | + const message = { |
| 113 | + type: "say", |
| 114 | + reasoning: "<thinking>This is my reasoning</thinking>", |
| 115 | + } |
| 116 | + |
| 117 | + const escaped = escapeClineMessage(message) |
| 118 | + expect(escaped.reasoning).toBe("<thinking>This is my reasoning</thinking>") |
| 119 | + }) |
| 120 | + |
| 121 | + it("should handle images array", () => { |
| 122 | + const message = { |
| 123 | + type: "say", |
| 124 | + images: ["data:image/png;base64,iVBORw0KGgo...", '<img src="xss">', "https://example.com/image.png"], |
| 125 | + } |
| 126 | + |
| 127 | + const escaped = escapeClineMessage(message) |
| 128 | + expect(escaped.images[0]).toBe("data:image/png;base64,iVBORw0KGgo...") // Data URIs not escaped |
| 129 | + expect(escaped.images[1]).toBe("<img src="xss">") |
| 130 | + expect(escaped.images[2]).toBe("https://example.com/image.png") |
| 131 | + }) |
| 132 | + |
| 133 | + it("should handle null or undefined messages", () => { |
| 134 | + expect(escapeClineMessage(null)).toBe(null) |
| 135 | + expect(escapeClineMessage(undefined)).toBe(undefined) |
| 136 | + }) |
| 137 | + |
| 138 | + it("should not modify original message", () => { |
| 139 | + const message = { |
| 140 | + text: "<div>Hello</div>", |
| 141 | + } |
| 142 | + |
| 143 | + const escaped = escapeClineMessage(message) |
| 144 | + expect(message.text).toBe("<div>Hello</div>") |
| 145 | + expect(escaped.text).toBe("<div>Hello</div>") |
| 146 | + }) |
| 147 | + }) |
| 148 | +}) |
0 commit comments