Skip to content

Commit 5eab7f9

Browse files
test: collapse conftest fixtures and use real DOMPurify in JS tests
Extract _make_test_client() for shared Flask client seeding. Run markdown XSS tests against dompurify/marked dev deps (CDN versions) instead of a regex mock sanitize implementation.
1 parent d7bc647 commit 5eab7f9

4 files changed

Lines changed: 72 additions & 32 deletions

File tree

package-lock.json

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
},
99
"devDependencies": {
1010
"@vitest/coverage-v8": "^3.2.4",
11+
"dompurify": "^3.2.7",
1112
"jsdom": "^26.1.0",
13+
"marked": "^12.0.1",
1214
"vitest": "^3.2.4"
1315
}
1416
}

static/js/shared/markdown.test.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import DOMPurify from 'dompurify';
3+
import { marked } from 'marked';
24
import { cleanContent, renderMarkdown } from './markdown.js';
35

46
const _origMarked = globalThis.marked;
@@ -17,24 +19,24 @@ describe('cleanContent', () => {
1719

1820
describe('renderMarkdown', () => {
1921
beforeEach(() => {
20-
globalThis.marked = {
21-
parse: vi.fn((text) => `<p>${text}</p>`),
22-
};
23-
globalThis.DOMPurify = {
24-
sanitize: vi.fn((html) => html.replace(/<script[\s\S]*?<\/script>/gi, '')),
25-
};
22+
globalThis.marked = marked;
23+
globalThis.DOMPurify = DOMPurify;
2624
});
2725

2826
afterEach(() => {
29-
vi.restoreAllMocks();
3027
globalThis.marked = _origMarked;
3128
globalThis.DOMPurify = _origDOMPurify;
3229
});
3330

3431
it('sanitizes script tags from parsed output', () => {
35-
globalThis.marked.parse.mockReturnValue('<p>ok</p><script>alert(1)</script>');
36-
const html = renderMarkdown('# Hello');
32+
const html = renderMarkdown('# Hello\n\n<script>alert(1)</script>');
3733
expect(html).not.toContain('<script');
34+
expect(html).not.toMatch(/alert\s*\(/);
35+
});
36+
37+
it('strips event handlers from parsed output', () => {
38+
const html = renderMarkdown('<img src=x onerror=alert(1)>');
39+
expect(html).not.toMatch(/onerror/i);
3840
});
3941

4042
it('falls back to inline code when marked is unavailable', () => {

tests/conftest.py

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import shutil
6+
from collections.abc import Mapping
67
from pathlib import Path
78

89
import pytest
@@ -12,43 +13,45 @@
1213
FIXTURES = Path(__file__).parent / "fixtures"
1314

1415

15-
@pytest.fixture
16-
def client(tmp_path):
17-
"""Flask test client with two seeded sessions in 'test-project'."""
18-
project_dir = tmp_path / "test-project"
19-
project_dir.mkdir(parents=True)
20-
shutil.copy(FIXTURES / "session_minimal.jsonl", project_dir / "session_abc123.jsonl")
21-
shutil.copy(FIXTURES / "session_with_tools.jsonl", project_dir / "session_def456.jsonl")
16+
def _make_test_client(tmp_path, session_files: Mapping[str, str] | None = None):
17+
"""Build a Flask test client, optionally seeding session JSONL files under test-project."""
18+
if session_files:
19+
project_dir = tmp_path / "test-project"
20+
project_dir.mkdir(parents=True)
21+
for dest_name, fixture_name in session_files.items():
22+
shutil.copy(FIXTURES / fixture_name, project_dir / dest_name)
2223
app = create_app(base_dir=str(tmp_path))
2324
app.config["TESTING"] = True
2425
return app.test_client()
2526

2627

28+
@pytest.fixture
29+
def client(tmp_path):
30+
"""Flask test client with two seeded sessions in 'test-project'."""
31+
return _make_test_client(
32+
tmp_path,
33+
{
34+
"session_abc123.jsonl": "session_minimal.jsonl",
35+
"session_def456.jsonl": "session_with_tools.jsonl",
36+
},
37+
)
38+
39+
2740
@pytest.fixture
2841
def client_single(tmp_path):
2942
"""Flask test client with one seeded session — for search/limit tests."""
30-
project_dir = tmp_path / "test-project"
31-
project_dir.mkdir(parents=True)
32-
shutil.copy(FIXTURES / "session_minimal.jsonl", project_dir / "session_abc123.jsonl")
33-
app = create_app(base_dir=str(tmp_path))
34-
app.config["TESTING"] = True
35-
return app.test_client()
43+
return _make_test_client(tmp_path, {"session_abc123.jsonl": "session_minimal.jsonl"})
3644

3745

3846
@pytest.fixture
3947
def client_empty(tmp_path):
4048
"""Flask test client with an empty projects directory."""
41-
app = create_app(base_dir=str(tmp_path))
42-
app.config["TESTING"] = True
43-
return app.test_client()
49+
return _make_test_client(tmp_path)
4450

4551

4652
@pytest.fixture
4753
def client_thinking(tmp_path):
4854
"""Flask test client with a session containing thinking content blocks."""
49-
project_dir = tmp_path / "test-project"
50-
project_dir.mkdir(parents=True)
51-
shutil.copy(FIXTURES / "session_with_thinking.jsonl", project_dir / "session_think001.jsonl")
52-
app = create_app(base_dir=str(tmp_path))
53-
app.config["TESTING"] = True
54-
return app.test_client()
55+
return _make_test_client(
56+
tmp_path, {"session_think001.jsonl": "session_with_thinking.jsonl"}
57+
)

0 commit comments

Comments
 (0)