Skip to content

Commit

Permalink
Enhance: Expand Receipt Search Functionality and Add Unit Tests (#400)
Browse files Browse the repository at this point in the history
* feat: expand receipt search functionality

Enhanced the receipt fetch search query to include additional fields. Updated the search logic to filter results by `merchant_store`, `purchase_category`, and `id`. Previously, the search functionality only filtered by `name` and `date`. The updated implementation provides a more comprehensive receipt search.

* chore: add unit tests for receipt search

This commit introduces unit tests for the receipt search functionality performed via the receipts management dashboard. Specifically, this commit adds two `test_search_functionality` unit tests in the `views` and `api` test folders' `test_receipts.py` files to test various search queries, ensuring the receipt search feature works correctly across various scenarios, including exact matches, substring matches, case insensitivity, empty queries, and nonexistent queries.

* chore: revamp receipt search test query substrings

Slightly readjusted receipt search unit test query substrings to pass the `typos` grammar check.
  • Loading branch information
artkolpakov authored Jun 7, 2024
1 parent 7f2e8e5 commit 583dfd0
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 1 deletion.
8 changes: 7 additions & 1 deletion backend/api/receipts/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ def fetch_all_receipts(request: HtmxHttpRequest):
results = results.filter(user=request.user)

if search_text:
results = results.filter(Q(name__icontains=search_text) | Q(date__icontains=search_text)).order_by("-date")
results = results.filter(
Q(name__icontains=search_text)
| Q(date__icontains=search_text)
| Q(merchant_store__icontains=search_text)
| Q(purchase_category__icontains=search_text)
| Q(id__icontains=search_text)
).order_by("-date")
elif selected_filters:
context.update({"selected_filters": [selected_filters]})
results = results.filter(total_price__gte=selected_filters).order_by("-date")
Expand Down
93 changes: 93 additions & 0 deletions tests/api/test_receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,96 @@ def test_clients_get_returned(self):
# Check that all created clients are in the response
for receipt in receipts:
self.assertIn(receipt, response.context.get("receipts"))

def test_search_functionality(self):
# Log in the user
self.login_user()

# Create some receipts with different names, dates, merchant stores, purchase categories, and IDs
receipt1 = baker.make(
"backend.Receipt",
name="Groceries",
date="2022-06-01",
merchant_store="Walmart",
purchase_category="Food",
id=1,
user=self.log_in_user,
)
receipt2 = baker.make(
"backend.Receipt",
name="Electronics",
date="2021-06-02",
merchant_store="Best Buy",
purchase_category="Gadgets",
id=2,
user=self.log_in_user,
)
receipt3 = baker.make(
"backend.Receipt",
name="Clothing",
date="2023-05-03",
merchant_store="Gap",
purchase_category="Apparel",
id=3,
user=self.log_in_user,
)

# Define the URL with the search query parameter
url = reverse(self.url_name)
headers = {"HTTP_HX-Request": "true"}

# Test searching by name
response = self.client.get(url, {"search": "Groceries"}, **headers)
self.assertEqual(response.status_code, 200)
self.assertIn(receipt1, response.context["receipts"])
self.assertNotIn(receipt2, response.context["receipts"])
self.assertNotIn(receipt3, response.context["receipts"])

# Test searching by date
response = self.client.get(url, {"search": "2021-06-02"}, **headers)
self.assertEqual(response.status_code, 200)
self.assertIn(receipt2, response.context["receipts"])
self.assertNotIn(receipt1, response.context["receipts"])
self.assertNotIn(receipt3, response.context["receipts"])

# Test searching by merchant/store
response = self.client.get(url, {"search": "Best Buy"}, **headers)
self.assertEqual(response.status_code, 200)
self.assertIn(receipt2, response.context["receipts"])
self.assertNotIn(receipt1, response.context["receipts"])
self.assertNotIn(receipt3, response.context["receipts"])

# Test searching by purchase category
response = self.client.get(url, {"search": "Apparel"}, **headers)
self.assertEqual(response.status_code, 200)
self.assertIn(receipt3, response.context["receipts"])
self.assertNotIn(receipt1, response.context["receipts"])
self.assertNotIn(receipt2, response.context["receipts"])

# Test searching by ID
response = self.client.get(url, {"search": "3"}, **headers)
self.assertEqual(response.status_code, 200)
self.assertIn(receipt3, response.context["receipts"])
self.assertNotIn(receipt1, response.context["receipts"])
self.assertNotIn(receipt2, response.context["receipts"])

# Test searching with a substring
response = self.client.get(url, {"search": "Elec"}, **headers)
self.assertEqual(response.status_code, 200)
self.assertIn(receipt2, response.context["receipts"])
self.assertNotIn(receipt1, response.context["receipts"])
self.assertNotIn(receipt3, response.context["receipts"])

# Test searching with a query that matches multiple receipt records
response = self.client.get(url, {"search": "6"}, **headers)
self.assertEqual(response.status_code, 200)
self.assertIn(receipt1, response.context["receipts"])
self.assertIn(receipt2, response.context["receipts"])
self.assertNotIn(receipt3, response.context["receipts"])

# Test searching with an empty query
response = self.client.get(url, {"search": ""}, **headers)
self.assertEqual(response.status_code, 200)
self.assertIn(receipt1, response.context["receipts"])
self.assertIn(receipt2, response.context["receipts"])
self.assertIn(receipt3, response.context["receipts"])
67 changes: 67 additions & 0 deletions tests/views/test_receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from backend.models import Receipt
from tests.handler import ViewTestCase
from model_bakery import baker


class ReceiptsViewTestCase(ViewTestCase):
Expand Down Expand Up @@ -31,6 +32,72 @@ def test_receipts_dashboard_view_matches_with_urls_view(self):
self.assertEqual("/dashboard/receipts/", self._receipts_dashboard_url)
self.assertEqual("backend.views.core.receipts.dashboard.receipts_dashboard", func_name)

def test_search_functionality(self):
self.login_user()

# Create some receipts with different names, dates, merchant stores, purchase categories, and IDs
receipt_attributes = [
{"name": "Groceries", "date": "2024-02-01", "merchant_store": "Walmart", "purchase_category": "Food", "id": 1},
{"name": "Electronics", "date": "2023-08-02", "merchant_store": "Best Buy", "purchase_category": "Gadgets", "id": 2},
{"name": "Clothing", "date": "2021-01-05", "merchant_store": "Gap", "purchase_category": "Apparel", "id": 3},
{"name": "Groceries Deluxe", "date": "2020-09-04", "merchant_store": "Whole Foods", "purchase_category": "Food", "id": 4},
{"name": "Gadgets Plus", "date": "2022-12-05", "merchant_store": "Apple Store", "purchase_category": "Gadgets", "id": 5},
{"name": "Special Groceries", "date": "2023-07-07", "merchant_store": "Trader Joe's", "purchase_category": "Food", "id": 6},
]
receipts = [baker.make("backend.Receipt", user=self.log_in_user, **attrs) for attrs in receipt_attributes]

# Define the URL with the search query parameter
url = reverse("api:receipts:fetch")
headers = {"HTTP_HX-Request": "true"}

# Define search queries to cover various edge cases
search_queries = [
# Exact matches
{"query": "Groceries", "expected_receipts": [receipts[0], receipts[3], receipts[5]]},
{"query": "2022-12-05", "expected_receipts": [receipts[4]]},
{"query": "Best Buy", "expected_receipts": [receipts[1]]},
{"query": "Apparel", "expected_receipts": [receipts[2]]},
{"query": "6", "expected_receipts": [receipts[5]]},
# Substring matches
{"query": "Electroni", "expected_receipts": [receipts[1]]},
{"query": "Who", "expected_receipts": [receipts[3]]},
{"query": "Gadge", "expected_receipts": [receipts[1], receipts[4]]},
{"query": "Foo", "expected_receipts": [receipts[0], receipts[3], receipts[5]]},
{"query": "2023", "expected_receipts": [receipts[1], receipts[5]]},
# Case insensitivity
{"query": "gadgets plus", "expected_receipts": [receipts[4]]},
{"query": "CLOTHING", "expected_receipts": [receipts[2]]},
{"query": "groCEries deLuXe", "expected_receipts": [receipts[3]]},
{"query": "WaLmArT", "expected_receipts": [receipts[0]]},
{"query": "TradeR Joe'S", "expected_receipts": [receipts[5]]},
# Empty query
{"query": "", "expected_receipts": receipts},
# Nonexistent query
{"query": "NonExistentReceiptName", "expected_receipts": []},
{"query": "Walmartt", "expected_receipts": []},
{"query": "nonexistentstore", "expected_receipts": []},
{"query": "10", "expected_receipts": []},
]

for search in search_queries:
response = self.client.get(url, {"search": search["query"]}, **headers)
self.assertEqual(response.status_code, 200)

# Verify that the "receipts" context variable is set
returned_receipts = response.context.get("receipts")
self.assertIsNotNone(returned_receipts, f"Context variable 'receipts' should not be None for query: {search['query']}")

# Convert QuerySet to list for easy comparison
returned_receipts_list = list(returned_receipts)

# Verify that the returned receipts match the expected receipts
expected_receipts = search["expected_receipts"]
self.assertEqual(
len(returned_receipts_list), len(expected_receipts), f"Mismatch in number of receipts for query: {search['query']}"
)
for receipt in expected_receipts:
self.assertIn(receipt, returned_receipts_list, f"Receipt {receipt} should be in the response for query: {search['query']}")


class ReceiptsAPITestCase(ViewTestCase):
def setUp(self):
Expand Down

0 comments on commit 583dfd0

Please sign in to comment.