Skip to content

Commit a03b674

Browse files
committed
feat: add B4 tests for cheatsheet extractor
Signed-off-by: Abhijeet Saharan <abhijeetsaharan2236@gmail.com>
1 parent 2ba1f4b commit a03b674

2 files changed

Lines changed: 181 additions & 4 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import unittest
2+
from application.utils.external_project_parsers.parsers.cheatsheet_extractor import (
3+
extract_cheatsheet_record,
4+
)
5+
from application.defs.cheatsheet_defs import SUMMARY_MAX_LENGTH
6+
7+
SOURCE_PATH = "cheatsheets/Secrets_Management_Cheat_Sheet.md"
8+
EXPECTED_SOURCE_ID = "Secrets_Management_Cheat_Sheet"
9+
EXPECTED_HYPERLINK = "https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"
10+
11+
NORMAL_MD = """\
12+
# Secrets Management Cheat Sheet
13+
14+
## Introduction
15+
Storage guidance.
16+
17+
## Architectural Patterns
18+
Use vaults and environment isolation.
19+
"""
20+
21+
MISSING_H1_MD = """\
22+
## Introduction
23+
No H1 present.
24+
25+
## Details
26+
More content.
27+
"""
28+
29+
EMPTY_MD = ""
30+
31+
# No ## headings — _extract_summary raises, _fallback_summary matches # via _ANY_HEADING_RE
32+
# and extracts body until len(markdown) since there is no next heading.
33+
BODY_UNDER_H1_MD = """\
34+
# Single Heading Cheat Sheet
35+
36+
Body text directly under H1, no subheadings at all.
37+
"""
38+
39+
# Leading spaces before # and ##malformed (no space) — both handled by \s* and (?!#) regex
40+
MALFORMED_MD = """\
41+
# Malformed Title
42+
43+
##malformed
44+
45+
## Introduction
46+
Some intro text.
47+
48+
## Valid Heading
49+
"""
50+
51+
52+
class TestNormal(unittest.TestCase):
53+
def setUp(self):
54+
self.record = extract_cheatsheet_record(NORMAL_MD, SOURCE_PATH)
55+
56+
# source, source_id, hyperlink, raw_markdown_path are derived from SOURCE_PATH
57+
# and are independent of markdown content — verified once here for all cases
58+
def test_source(self):
59+
self.assertEqual(self.record.source, "owasp_cheatsheets")
60+
61+
def test_source_id(self):
62+
self.assertEqual(self.record.source_id, EXPECTED_SOURCE_ID)
63+
64+
def test_hyperlink(self):
65+
self.assertEqual(self.record.hyperlink, EXPECTED_HYPERLINK)
66+
67+
def test_raw_markdown_path(self):
68+
self.assertEqual(self.record.raw_markdown_path, SOURCE_PATH)
69+
70+
def test_title(self):
71+
self.assertEqual(self.record.title, "Secrets Management Cheat Sheet")
72+
73+
def test_summary(self):
74+
self.assertEqual(self.record.summary, "Storage guidance.")
75+
76+
def test_summary_bounded(self):
77+
# SUMMARY_MAX_LENGTH truncation happens in CheatsheetRecord.__post_init__
78+
# for every record — testing once here covers all cases
79+
self.assertLessEqual(len(self.record.summary), SUMMARY_MAX_LENGTH)
80+
81+
def test_headings(self):
82+
self.assertIn("Introduction", self.record.headings)
83+
self.assertIn("Architectural Patterns", self.record.headings)
84+
85+
def test_fallback_not_used(self):
86+
self.assertEqual(self.record.metadata["fallback_used"], "false")
87+
88+
89+
class TestMissingH1(unittest.TestCase):
90+
def setUp(self):
91+
self.record = extract_cheatsheet_record(MISSING_H1_MD, SOURCE_PATH)
92+
93+
def test_title_is_fallback(self):
94+
self.assertEqual(self.record.title, "No title found.")
95+
96+
def test_summary_from_introduction(self):
97+
self.assertIn("no h1", self.record.summary.lower())
98+
99+
def test_headings_extracted(self):
100+
self.assertIn("Introduction", self.record.headings)
101+
self.assertIn("Details", self.record.headings)
102+
103+
def test_fallback_used(self):
104+
self.assertEqual(self.record.metadata["fallback_used"], "true")
105+
106+
107+
class TestEmptyMarkdown(unittest.TestCase):
108+
def setUp(self):
109+
self.record = extract_cheatsheet_record(EMPTY_MD, SOURCE_PATH)
110+
111+
def test_title_is_fallback(self):
112+
self.assertEqual(self.record.title, "No title found.")
113+
114+
def test_summary_no_summary_found(self):
115+
# No headings at all — _fallback_summary returns this literal string
116+
self.assertEqual(self.record.summary, "No summary found.")
117+
118+
def test_headings_empty(self):
119+
self.assertEqual(self.record.headings, [])
120+
121+
def test_fallback_used(self):
122+
self.assertEqual(self.record.metadata["fallback_used"], "true")
123+
124+
125+
class TestBodyUnderH1(unittest.TestCase):
126+
def setUp(self):
127+
self.record = extract_cheatsheet_record(BODY_UNDER_H1_MD, SOURCE_PATH)
128+
129+
def test_title(self):
130+
self.assertEqual(self.record.title, "Single Heading Cheat Sheet")
131+
132+
def test_summary_from_fallback_via_h1(self):
133+
# _fallback_summary matches # heading, extracts body until len(markdown)
134+
self.assertIn("body text", self.record.summary.lower())
135+
136+
def test_headings_empty(self):
137+
# _HEADING_RE only matches ## — no ## present here
138+
self.assertEqual(self.record.headings, [])
139+
140+
def test_fallback_used(self):
141+
self.assertEqual(self.record.metadata["fallback_used"], "true")
142+
143+
144+
class TestMalformedHeadings(unittest.TestCase):
145+
def setUp(self):
146+
self.record = extract_cheatsheet_record(MALFORMED_MD, SOURCE_PATH)
147+
148+
def test_malformed_h1_extracted(self):
149+
self.assertEqual(self.record.title, "Malformed Title")
150+
151+
def test_malformed_h2_in_headings(self):
152+
self.assertIn("malformed", self.record.headings)
153+
154+
def test_valid_headings_also_extracted(self):
155+
self.assertIn("Introduction", self.record.headings)
156+
self.assertIn("Valid Heading", self.record.headings)
157+
158+
def test_summary_from_introduction(self):
159+
self.assertIn("intro", self.record.summary.lower())
160+
161+
def test_fallback_not_used(self):
162+
self.assertEqual(self.record.metadata["fallback_used"], "false")
163+
164+
165+
if __name__ == "__main__":
166+
unittest.main()

application/utils/external_project_parsers/parsers/cheatsheet_extractor.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,20 @@
88

99
CANONICAL_BASE_URL = "https://cheatsheetseries.owasp.org/cheatsheets/"
1010

11-
_TITLE_RE = re.compile(r"^#\s+(?P<title>.+)$", re.MULTILINE)
12-
_HEADING_RE = re.compile(r"^##\s+(?P<heading>.+)$", re.MULTILINE)
13-
_ANY_HEADING_RE = re.compile(r"^#{1,6}\s+.+$", re.MULTILINE)
11+
_TITLE_RE = re.compile(
12+
r"^\s*#(?!#)\s*(?P<title>.+?)$",
13+
re.MULTILINE,
14+
)
15+
16+
_HEADING_RE = re.compile(
17+
r"^\s*##(?!#)\s*(?P<heading>.+?)$",
18+
re.MULTILINE,
19+
)
20+
21+
_ANY_HEADING_RE = re.compile(
22+
r"^\s*#{1,6}(?!#)\s*.+?$",
23+
re.MULTILINE,
24+
)
1425

1526

1627
def _derive_source_id(source_path: str) -> str:
@@ -44,7 +55,7 @@ def _extract_summary(markdown: str) -> str:
4455
"""Extract summary from Introduction section in cheatsheet markdown."""
4556

4657
for match in _ANY_HEADING_RE.finditer(markdown):
47-
if match.group().lstrip("#").strip().lower() == "introduction":
58+
if match.group().strip().lstrip("#").strip().lower() == "introduction":
4859
body = _extract_body_after_heading(markdown, match)
4960
if body:
5061
return body

0 commit comments

Comments
 (0)