Find a knowledge base article by words from different fields
This commit is contained in:
128
tests/test_plugins/test_knowledgebase_search_words.py
Normal file
128
tests/test_plugins/test_knowledgebase_search_words.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Searching a knowledge base article by words that live in different fields.
|
||||
|
||||
An article's searchable text is spread across three places: its title, its
|
||||
keywords, and the name of its topic. People do not know or care which word came
|
||||
from which, so they type the two words that identify the article and expect it
|
||||
back - 'CMM Community' for an article tagged `community` under the CMM topic.
|
||||
|
||||
Both search paths used one `ilike('%CMM Community%')`, which needs that exact
|
||||
phrase contiguous in a SINGLE field. No article has it, so the search returned
|
||||
nothing at all and the article looked absent rather than unfindable. Every word
|
||||
must be found somewhere, in any order, across all three fields.
|
||||
|
||||
Covers the plugin's own listing (`GET /api/knowledgebase?search=`, what the KB
|
||||
page uses) and global search (`GET /api/search?q=`), which had the fault in two
|
||||
different shapes: the listing did not split words, and global search split them
|
||||
but never looked at the topic.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from shopdb.core.models import Application
|
||||
from plugins.knowledgebase.models import KnowledgeBase
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def library(db):
|
||||
"""Articles whose identifying words are deliberately spread across fields."""
|
||||
topics = {
|
||||
'cmm': Application(appname='Zzcmm', isactive=True),
|
||||
'other': Application(appname='Zzunrelated', isactive=True),
|
||||
'retired': Application(appname='Zzretired Zzcmm', isactive=False),
|
||||
}
|
||||
db.session.add_all(topics.values())
|
||||
db.session.flush()
|
||||
|
||||
articles = {
|
||||
# The reported case: neither word is in the same field as the other.
|
||||
'split': KnowledgeBase(
|
||||
shortdescription='Zzprotect viewer notes', linkurl='u1',
|
||||
keywords='zzcommunity license', appid=topics['cmm'].appid,
|
||||
isactive=True),
|
||||
# Both words present but not adjacent, inside one field.
|
||||
'gapped': KnowledgeBase(
|
||||
shortdescription='Zzpower outage plan alert zznotification',
|
||||
linkurl='u2', keywords='', appid=None, isactive=True),
|
||||
# Holds only one of the two words; must NOT come back.
|
||||
'halfmatch': KnowledgeBase(
|
||||
shortdescription='Zzprotect viewer only', linkurl='u3',
|
||||
keywords='license', appid=topics['other'].appid, isactive=True),
|
||||
# Retired topic, matches both words. The existing rule still wins.
|
||||
'retired': KnowledgeBase(
|
||||
shortdescription='Zzprotect viewer retired', linkurl='u4',
|
||||
keywords='zzcommunity', appid=topics['retired'].appid,
|
||||
isactive=True),
|
||||
}
|
||||
db.session.add_all(articles.values())
|
||||
db.session.commit()
|
||||
return {'topics': topics, 'articles': articles}
|
||||
|
||||
|
||||
def listed(client, search):
|
||||
"""Titles from the plugin's own listing, the one the KB page calls."""
|
||||
resp = client.get('/api/knowledgebase', query_string={'search': search})
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
return [row['shortdescription'] for row in resp.get_json()['data']]
|
||||
|
||||
|
||||
def globally(client, search):
|
||||
"""Knowledge base titles from global search."""
|
||||
resp = client.get('/api/search', query_string={'q': search})
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
return [r['title'] for r in resp.get_json()['data']['results']
|
||||
if r['type'] == 'knowledgebase']
|
||||
|
||||
|
||||
# --- the reported case ------------------------------------------------------
|
||||
|
||||
def test_listing_finds_an_article_by_its_topic_and_its_keyword(client, library):
|
||||
"""'Zzcmm Zzcommunity': one word from the topic's name, one from the keywords.
|
||||
|
||||
This is the bug as reported. A phrase match could satisfy neither field.
|
||||
"""
|
||||
assert 'Zzprotect viewer notes' in listed(client, 'Zzcmm Zzcommunity')
|
||||
|
||||
|
||||
def test_global_search_finds_an_article_by_its_topic_and_its_keyword(client, library):
|
||||
"""Global search split the words but only ever looked at title and keywords,
|
||||
so a word that only the topic could satisfy failed there too."""
|
||||
assert 'Zzprotect viewer notes' in globally(client, 'Zzcmm Zzcommunity')
|
||||
|
||||
|
||||
# --- word order and adjacency ----------------------------------------------
|
||||
|
||||
def test_word_order_does_not_matter(client, library):
|
||||
assert 'Zzprotect viewer notes' in listed(client, 'Zzcommunity Zzcmm')
|
||||
|
||||
|
||||
def test_words_need_not_be_adjacent_within_one_field(client, library):
|
||||
"""'plan' and 'alert' sit between them in the title."""
|
||||
assert 'Zzpower outage plan alert zznotification' in listed(
|
||||
client, 'Zzpower zznotification')
|
||||
|
||||
|
||||
def test_the_whole_phrase_still_matches(client, library):
|
||||
"""Splitting must not break the searches that already worked."""
|
||||
assert 'Zzprotect viewer notes' in listed(client, 'Zzprotect viewer')
|
||||
|
||||
|
||||
# --- what must still be excluded -------------------------------------------
|
||||
|
||||
def test_an_article_holding_only_one_word_is_not_returned(client, library):
|
||||
"""EVERY word must be found. ORing them would return most of the library and
|
||||
bury the article the words actually describe."""
|
||||
found = listed(client, 'Zzprotect Zzcommunity')
|
||||
assert 'Zzprotect viewer only' not in found
|
||||
assert 'Zzprotect viewer notes' in found
|
||||
|
||||
|
||||
def test_a_retired_topic_still_hides_the_article(client, library):
|
||||
"""Matching more words must not become a way past the retired rule."""
|
||||
assert 'Zzprotect viewer retired' not in listed(client, 'Zzprotect Zzcommunity')
|
||||
assert 'Zzprotect viewer retired' not in globally(client, 'Zzprotect Zzcommunity')
|
||||
|
||||
|
||||
def test_a_retired_topic_name_is_not_a_way_in(client, library):
|
||||
"""The retired application is named 'Zzretired Zzcmm'; its name matching a word
|
||||
must not surface its articles."""
|
||||
assert listed(client, 'Zzretired') == []
|
||||
Reference in New Issue
Block a user