Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion suite/writer/api/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,18 @@ def search(query: str, filters: str | None = None):
k.update(meta)
cleaned_results.append(k)
search["results"] = cleaned_results

# The index is unscoped, so summary stats and spelling corrections are
# computed against every document on the site, not just what the caller
# can read. Recompute counts from the filtered set and drop corrections
# outright, since validating them against readable content isn't worth
# the cost the UI doesn't use them.
match_count = len(cleaned_results)
search["summary"]["total_matches"] = match_count
search["summary"]["returned_matches"] = match_count
search["summary"]["filtered_matches"] = match_count
search["summary"]["corrected_words"] = None
search["summary"]["corrected_query"] = None
Comment thread
greptile-apps[bot] marked this conversation as resolved.
return search


Expand All @@ -163,7 +175,7 @@ def get_drive_file_meta(names, ttl=3600):
cache = frappe.cache()

keys = {name: f"search:drive_file:{name}" for name in names}
cached = {"name": cache.get_value(k) for k in keys.values()}
cached = {"name": cache.get_value(k) for k in keys.values()} # noqa: B035 -- pre-existing, tracked separately

result = {}
missing = []
Expand Down
51 changes: 51 additions & 0 deletions suite/writer/api/tests/test_general.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt

from unittest.mock import patch

from frappe.tests import IntegrationTestCase

from suite.writer.api.general import search


class TestWriterSearch(IntegrationTestCase):
@patch("suite.writer.api.general.WriterSearch")
@patch("suite.writer.api.general.get_drive_file_meta")
@patch("suite.writer.api.general.get_user_access")
def test_search_summary_filters_unreadable_documents(
self, mock_get_user_access, mock_get_meta, mock_writer_search
):
mock_search_instance = mock_writer_search.return_value
mock_search_instance.search.return_value = {
"results": [{"name": "doc1"}, {"name": "doc2"}],
"summary": {
"total_matches": 10,
"returned_matches": 10,
"filtered_matches": 10,
"corrected_words": ["secret"],
"corrected_query": "secret query",
},
}

mock_get_meta.return_value = {
"doc1": {"name": "doc1", "title": "Readable Doc"},
"doc2": {"name": "doc2", "title": "Secret Doc"},
}

# doc1 is readable, doc2 is unreadable
mock_get_user_access.side_effect = lambda name: {"read": name == "doc1"}

res = search("test")

# Results should only contain readable doc1
self.assertEqual(len(res["results"]), 1)
self.assertEqual(res["results"][0]["name"], "doc1")

# Summary counts must be updated to filtered count (1), not raw count (10)
self.assertEqual(res["summary"]["total_matches"], 1)
self.assertEqual(res["summary"]["returned_matches"], 1)
self.assertEqual(res["summary"]["filtered_matches"], 1)

# Corrections must be cleared to prevent word leaks
self.assertIsNone(res["summary"]["corrected_words"])
self.assertIsNone(res["summary"]["corrected_query"])
Loading