CodeQL, high: a polynomial regular expression on uncontrolled data. Both frontmatter parsers fenced the block with `\s*`, and `\s` matches the newline the fence line ends with — so the closing alternative was reachable two ways and every added line multiplied the backtracking. The pathological input is a body that opens `---` and never closes it, which is what a document looks like for as long as the user is typing that block: 20k lines took 2.4s, and the body is parsed on every save. Fencing on `[ \t]*` instead leaves one way to reach the newline and the same input takes no measurable time. A trailing space on the fence still parses; what is no longer accepted is a fence followed by blank lines before the YAML, which nothing writes. Both copies, and a timing regression test each side. ha-relevant: yes
154 lines
5.8 KiB
Python
154 lines
5.8 KiB
Python
"""Slugs, frontmatter parsing, and the two tree guards."""
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.db.models import Document
|
|
from app.services import doc_tree
|
|
|
|
|
|
def _doc(**kwargs) -> Document:
|
|
base = dict(kind="folder", title="T", slug="t", body="")
|
|
base.update(kwargs)
|
|
return Document(**base)
|
|
|
|
|
|
# ── slugs ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_slugify_lowercases_and_collapses_punctuation():
|
|
assert doc_tree.slugify("NAS-01 / main storage!") == "nas-01-main-storage"
|
|
|
|
|
|
def test_slugify_of_nothing_is_untitled():
|
|
assert doc_tree.slugify("") == "untitled"
|
|
assert doc_tree.slugify(" ") == "untitled"
|
|
assert doc_tree.slugify("!!!") == "untitled"
|
|
|
|
|
|
def test_slugify_truncates_a_long_title():
|
|
assert len(doc_tree.slugify("x" * 200)) == 80
|
|
|
|
|
|
async def test_unique_slug_suffixes_a_taken_sibling(db_session: AsyncSession):
|
|
db_session.add(_doc(title="Network", slug="network"))
|
|
await db_session.flush()
|
|
assert await doc_tree.unique_slug(db_session, "Network", parent_id=None) == "network-2"
|
|
|
|
|
|
async def test_unique_slug_keeps_counting_past_the_second(db_session: AsyncSession):
|
|
db_session.add(_doc(slug="network"))
|
|
db_session.add(_doc(slug="network-2"))
|
|
await db_session.flush()
|
|
assert await doc_tree.unique_slug(db_session, "Network", parent_id=None) == "network-3"
|
|
|
|
|
|
async def test_unique_slug_only_looks_at_siblings(db_session: AsyncSession):
|
|
parent = _doc(title="Folder", slug="folder")
|
|
db_session.add(parent)
|
|
await db_session.flush()
|
|
db_session.add(_doc(slug="network", parent_id=parent.id))
|
|
await db_session.flush()
|
|
# Same slug, different parent — no clash.
|
|
assert await doc_tree.unique_slug(db_session, "Network", parent_id=None) == "network"
|
|
|
|
|
|
async def test_unique_slug_ignores_the_document_being_renamed(db_session: AsyncSession):
|
|
doc = _doc(title="Network", slug="network")
|
|
db_session.add(doc)
|
|
await db_session.flush()
|
|
assert await doc_tree.unique_slug(db_session, "Network", parent_id=None, exclude_id=doc.id) == "network"
|
|
|
|
|
|
# ── frontmatter ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_parse_frontmatter_reads_the_leading_block():
|
|
body = "---\ntitle: NAS\ntags: [a, b]\n---\n\n# NAS\n"
|
|
assert doc_tree.parse_frontmatter(body) == {"title": "NAS", "tags": ["a", "b"]}
|
|
|
|
|
|
def test_parse_frontmatter_ignores_a_block_that_is_not_first():
|
|
assert doc_tree.parse_frontmatter("# Title\n\n---\ntitle: NAS\n---\n") == {}
|
|
|
|
|
|
def test_parse_frontmatter_of_malformed_yaml_is_empty_not_an_error():
|
|
# The user is mid-edit; the cache goes stale, the save does not fail.
|
|
assert doc_tree.parse_frontmatter("---\ntitle: [unclosed\n---\nbody") == {}
|
|
|
|
|
|
def test_parse_frontmatter_of_a_non_mapping_is_empty():
|
|
assert doc_tree.parse_frontmatter("---\n- a\n- b\n---\n") == {}
|
|
|
|
|
|
def test_parse_frontmatter_of_an_empty_body_is_empty():
|
|
assert doc_tree.parse_frontmatter("") == {}
|
|
|
|
|
|
def test_parse_frontmatter_keeps_a_trailing_space_on_the_fence():
|
|
assert doc_tree.parse_frontmatter("--- \ntitle: NAS\n--- \n") == {"title": "NAS"}
|
|
|
|
|
|
def test_parse_frontmatter_of_an_unclosed_block_does_not_go_quadratic():
|
|
"""A body that opens a block and never closes it must still be linear.
|
|
|
|
The shape a document has while the block is being typed, and the input the
|
|
scanner named: `---\n` then many repetitions of `\n `. With `\s*` around the
|
|
fences the closing alternative was reachable two ways and each added line
|
|
multiplied the backtracking.
|
|
"""
|
|
import time
|
|
|
|
body = "---\n" + "\n " * 40_000
|
|
started = time.perf_counter()
|
|
assert doc_tree.parse_frontmatter(body) == {}
|
|
assert time.perf_counter() - started < 1.0
|
|
|
|
|
|
def test_tags_accepts_a_list_or_a_comma_string():
|
|
assert doc_tree.tags_from({"tags": ["a", " b "]}) == ["a", "b"]
|
|
assert doc_tree.tags_from({"tags": "a, b"}) == ["a", "b"]
|
|
assert doc_tree.tags_from({"tags": 7}) == []
|
|
assert doc_tree.tags_from({}) == []
|
|
|
|
|
|
# ── tree guards ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
async def _chain(db_session: AsyncSession, depth: int) -> list[Document]:
|
|
docs: list[Document] = []
|
|
parent_id = None
|
|
for i in range(depth):
|
|
doc = _doc(title=f"L{i}", slug=f"l{i}", parent_id=parent_id)
|
|
db_session.add(doc)
|
|
await db_session.flush()
|
|
docs.append(doc)
|
|
parent_id = doc.id
|
|
return docs
|
|
|
|
|
|
async def test_is_ancestor_walks_the_whole_chain(db_session: AsyncSession):
|
|
a, b, c = await _chain(db_session, 3)
|
|
assert await doc_tree.is_ancestor(db_session, a.id, c.id)
|
|
assert not await doc_tree.is_ancestor(db_session, c.id, a.id)
|
|
|
|
|
|
async def test_a_document_is_its_own_ancestor(db_session: AsyncSession):
|
|
(a,) = await _chain(db_session, 1)
|
|
assert await doc_tree.is_ancestor(db_session, a.id, a.id)
|
|
|
|
|
|
async def test_subtree_ids_collects_every_descendant(db_session: AsyncSession):
|
|
a, b, c = await _chain(db_session, 3)
|
|
sibling = _doc(title="S", slug="s", parent_id=a.id)
|
|
db_session.add(sibling)
|
|
await db_session.flush()
|
|
ids = await doc_tree.subtree_ids(db_session, a.id)
|
|
assert set(ids) == {a.id, b.id, c.id, sibling.id}
|
|
# Root first, so a caller deleting in reverse removes leaves first.
|
|
assert ids[0] == a.id
|
|
|
|
|
|
async def test_subtree_ids_of_a_leaf_is_just_itself(db_session: AsyncSession):
|
|
(a,) = await _chain(db_session, 1)
|
|
assert await doc_tree.subtree_ids(db_session, a.id) == [a.id]
|