diff --git a/tests/test_strongs.py b/tests/test_strongs.py new file mode 100644 index 0000000..e97ebf5 --- /dev/null +++ b/tests/test_strongs.py @@ -0,0 +1,490 @@ +""" +Tests for Strong's Concordance functionality. + +Tests cover: +- Strong's data loading and lookup +- Hebrew and Greek entry retrieval +- Search functionality +- Pagination +- Web routes +- API endpoints +""" + +import pytest +from fastapi.testclient import TestClient + +from kjvstudy_org.server import app +from kjvstudy_org.strongs import ( + get_strongs_entry, + get_strongs_definition, + get_strongs_word, + get_strongs_transliteration, + get_strongs_kjv_usage, + format_strongs_entry, + search_strongs, + get_all_strongs, +) + + +@pytest.fixture +def client(): + """Create test client""" + return TestClient(app) + + +class TestStrongsDataLoading: + """Tests for Strong's data loading and basic lookup""" + + def test_get_hebrew_entry(self): + """Test retrieving a Hebrew Strong's entry""" + entry = get_strongs_entry("H1") + assert entry is not None + assert "lemma" in entry + + def test_get_greek_entry(self): + """Test retrieving a Greek Strong's entry""" + entry = get_strongs_entry("G1") + assert entry is not None + assert "lemma" in entry + + def test_get_entry_case_insensitive(self): + """Test Strong's lookup is case-insensitive""" + entry1 = get_strongs_entry("H430") + entry2 = get_strongs_entry("h430") + assert entry1 == entry2 + + def test_get_entry_with_whitespace(self): + """Test Strong's lookup handles whitespace""" + entry = get_strongs_entry(" H1 ") + assert entry is not None + + def test_get_invalid_entry(self): + """Test invalid Strong's number returns None""" + entry = get_strongs_entry("X9999") + assert entry is None + + def test_get_nonexistent_entry(self): + """Test nonexistent Strong's number returns None""" + entry = get_strongs_entry("H99999") + assert entry is None + + def test_get_empty_entry(self): + """Test empty string returns None""" + entry = get_strongs_entry("") + assert entry is None + + def test_get_none_entry(self): + """Test None input returns None""" + entry = get_strongs_entry(None) + assert entry is None + + +class TestStrongsHelperFunctions: + """Tests for Strong's helper functions""" + + def test_get_definition(self): + """Test getting Strong's definition""" + definition = get_strongs_definition("G26") + assert definition is not None + assert len(definition) > 0 + + def test_get_word(self): + """Test getting original word (lemma)""" + word = get_strongs_word("G26") + assert word is not None + # Greek agape + assert "γάπ" in word or "agap" in word.lower() + + def test_get_transliteration(self): + """Test getting transliteration""" + translit = get_strongs_transliteration("G26") + assert translit is not None + # Transliteration is "agápē" - check for base characters + assert "ag" in translit.lower() and "p" in translit.lower() + + def test_get_kjv_usage(self): + """Test getting KJV usage""" + usage = get_strongs_kjv_usage("G26") + assert usage is not None + assert "love" in usage.lower() + + def test_get_hebrew_transliteration(self): + """Test Hebrew transliteration uses xlit field""" + translit = get_strongs_transliteration("H1") + assert translit is not None + + def test_helper_functions_invalid_number(self): + """Test helper functions handle invalid numbers""" + assert get_strongs_definition("X9999") is None + assert get_strongs_word("X9999") is None + assert get_strongs_transliteration("X9999") is None + assert get_strongs_kjv_usage("X9999") is None + + +class TestFormatStrongsEntry: + """Tests for format_strongs_entry function""" + + def test_format_greek_entry(self): + """Test formatting Greek entry""" + entry = format_strongs_entry("G26") + assert entry is not None + assert entry["strongs"] == "G26" + assert entry["language"] == "Greek" + assert "word" in entry + assert "definition" in entry + + def test_format_hebrew_entry(self): + """Test formatting Hebrew entry""" + entry = format_strongs_entry("H430") + assert entry is not None + assert entry["strongs"] == "H430" + assert entry["language"] == "Hebrew" + assert "word" in entry + assert "definition" in entry + + def test_format_entry_has_all_fields(self): + """Test formatted entry has all expected fields""" + entry = format_strongs_entry("G26") + expected_fields = [ + "strongs", "language", "word", "transliteration", + "definition", "kjv_usage", "derivation" + ] + for field in expected_fields: + assert field in entry + + def test_format_hebrew_has_pronunciation(self): + """Test Hebrew entries include pronunciation""" + entry = format_strongs_entry("H1") + assert "pronunciation" in entry + + def test_format_invalid_entry(self): + """Test formatting invalid entry returns None""" + entry = format_strongs_entry("X9999") + assert entry is None + + +class TestSearchStrongs: + """Tests for Strong's search functionality""" + + def test_search_finds_results(self): + """Test search returns results""" + results = search_strongs("love") + assert len(results) > 0 + + def test_search_result_structure(self): + """Test search results have correct structure""" + results = search_strongs("love", limit=1) + assert len(results) > 0 + result = results[0] + assert "strongs" in result + assert "language" in result + assert "word" in result + assert "definition" in result + + def test_search_hebrew_only(self): + """Test search Hebrew only""" + results = search_strongs("God", language="hebrew") + for result in results: + assert result["language"] == "Hebrew" + + def test_search_greek_only(self): + """Test search Greek only""" + results = search_strongs("love", language="greek") + for result in results: + assert result["language"] == "Greek" + + def test_search_both_languages(self): + """Test search both languages""" + results = search_strongs("peace", language="both") + languages = set(r["language"] for r in results) + # Should have at least one result + assert len(results) > 0 + + def test_search_respects_limit(self): + """Test search respects limit parameter""" + results = search_strongs("the", limit=5) + assert len(results) <= 5 + + def test_search_case_insensitive(self): + """Test search is case-insensitive""" + results1 = search_strongs("love") + results2 = search_strongs("LOVE") + assert len(results1) == len(results2) + + def test_search_no_results(self): + """Test search with no results""" + results = search_strongs("xyznonexistent123") + assert len(results) == 0 + + +class TestGetAllStrongs: + """Tests for paginated Strong's listing""" + + def test_get_all_hebrew(self): + """Test getting all Hebrew entries""" + data = get_all_strongs("hebrew", page=1, per_page=100) + assert "entries" in data + assert "total" in data + assert "page" in data + assert "total_pages" in data + assert len(data["entries"]) == 100 + assert data["total"] > 8000 # Should have ~8674 Hebrew entries + + def test_get_all_greek(self): + """Test getting all Greek entries""" + data = get_all_strongs("greek", page=1, per_page=100) + assert len(data["entries"]) == 100 + assert data["total"] > 5000 # Should have ~5523 Greek entries + + def test_pagination_page_2(self): + """Test pagination returns different entries""" + page1 = get_all_strongs("hebrew", page=1, per_page=10) + page2 = get_all_strongs("hebrew", page=2, per_page=10) + + # Entries should be different + page1_ids = [e["strongs"] for e in page1["entries"]] + page2_ids = [e["strongs"] for e in page2["entries"]] + assert page1_ids != page2_ids + + def test_pagination_total_pages(self): + """Test total pages calculation""" + data = get_all_strongs("hebrew", page=1, per_page=100) + expected_pages = (data["total"] + 99) // 100 + assert data["total_pages"] == expected_pages + + def test_entries_sorted_by_number(self): + """Test entries are sorted by Strong's number""" + data = get_all_strongs("hebrew", page=1, per_page=10) + numbers = [int(e["strongs"][1:]) for e in data["entries"]] + assert numbers == sorted(numbers) + + def test_invalid_language(self): + """Test invalid language returns empty""" + data = get_all_strongs("invalid", page=1) + assert data["entries"] == [] + assert data["total"] == 0 + + def test_entry_has_required_fields(self): + """Test paginated entries have required fields""" + data = get_all_strongs("greek", page=1, per_page=1) + entry = data["entries"][0] + assert "strongs" in entry + assert "language" in entry + assert "word" in entry + assert "transliteration" in entry + assert "definition" in entry + + +class TestStrongsWebRoutes: + """Tests for Strong's web routes""" + + def test_strongs_index_page(self, client): + """Test Strong's index page loads""" + response = client.get("/strongs") + assert response.status_code == 200 + content = response.content.decode() + assert "Strong" in content + + def test_strongs_search_page(self, client): + """Test Strong's search with query""" + response = client.get("/strongs?q=love") + assert response.status_code == 200 + content = response.content.decode() + assert "love" in content.lower() + + def test_strongs_entry_page_greek(self, client): + """Test Greek Strong's entry page""" + response = client.get("/strongs/G26") + assert response.status_code == 200 + content = response.content.decode() + assert "G26" in content + assert "Greek" in content + + def test_strongs_entry_page_hebrew(self, client): + """Test Hebrew Strong's entry page""" + response = client.get("/strongs/H430") + assert response.status_code == 200 + content = response.content.decode() + assert "H430" in content + assert "Hebrew" in content + + def test_strongs_entry_case_insensitive(self, client): + """Test entry page handles lowercase""" + response = client.get("/strongs/g26") + assert response.status_code == 200 + + def test_strongs_invalid_entry(self, client): + """Test invalid entry returns 404""" + response = client.get("/strongs/X9999") + assert response.status_code == 404 + + def test_strongs_hebrew_index(self, client): + """Test Hebrew index page""" + response = client.get("/strongs/hebrew") + assert response.status_code == 200 + content = response.content.decode() + assert "Hebrew" in content + assert "H1" in content + + def test_strongs_greek_index(self, client): + """Test Greek index page""" + response = client.get("/strongs/greek") + assert response.status_code == 200 + content = response.content.decode() + assert "Greek" in content + assert "G1" in content + + def test_strongs_hebrew_pagination(self, client): + """Test Hebrew index pagination""" + response = client.get("/strongs/hebrew?page=2") + assert response.status_code == 200 + content = response.content.decode() + assert "Page 2" in content + + def test_strongs_greek_pagination(self, client): + """Test Greek index pagination""" + response = client.get("/strongs/greek?page=2") + assert response.status_code == 200 + + def test_strongs_entry_has_verse_occurrences(self, client): + """Test entry page shows verse occurrences""" + # Use a common word that should have occurrences + response = client.get("/strongs/G2316") # theos (God) + assert response.status_code == 200 + content = response.content.decode() + # Should have occurrences section if interlinear data exists + assert "Occurrence" in content or "verse" in content.lower() + + +class TestStrongsAPIRoutes: + """Tests for Strong's API endpoints""" + + def test_api_strongs_entry(self, client): + """Test API Strong's entry endpoint""" + response = client.get("/api/strongs/G26") + assert response.status_code == 200 + data = response.json() + assert data["strongs"] == "G26" + assert data["language"] == "Greek" + + def test_api_strongs_hebrew_entry(self, client): + """Test API Hebrew Strong's entry""" + response = client.get("/api/strongs/H430") + assert response.status_code == 200 + data = response.json() + assert data["strongs"] == "H430" + assert data["language"] == "Hebrew" + + def test_api_strongs_invalid(self, client): + """Test API invalid Strong's number""" + response = client.get("/api/strongs/X9999") + assert response.status_code == 404 + + def test_api_strongs_search(self, client): + """Test API Strong's search endpoint""" + response = client.get("/api/strongs?q=love") + assert response.status_code == 200 + data = response.json() + assert "results" in data + assert len(data["results"]) > 0 + + +class TestStrongsEdgeCases: + """Edge case tests for Strong's functionality""" + + def test_first_hebrew_entry(self, client): + """Test first Hebrew entry (H1)""" + response = client.get("/strongs/H1") + assert response.status_code == 200 + + def test_last_hebrew_entry(self, client): + """Test last Hebrew entry (H8674)""" + response = client.get("/strongs/H8674") + assert response.status_code == 200 + + def test_first_greek_entry(self, client): + """Test first Greek entry (G1)""" + response = client.get("/strongs/G1") + assert response.status_code == 200 + + def test_last_greek_entry(self, client): + """Test last Greek entry (G5624)""" + response = client.get("/strongs/G5624") + assert response.status_code == 200 + + def test_hebrew_boundary_plus_one(self, client): + """Test beyond Hebrew range returns 404""" + response = client.get("/strongs/H9999") + assert response.status_code == 404 + + def test_greek_boundary_plus_one(self, client): + """Test beyond Greek range returns 404""" + response = client.get("/strongs/G9999") + assert response.status_code == 404 + + def test_common_hebrew_words(self, client): + """Test common Hebrew Strong's numbers""" + common = ["H430", "H3068", "H1", "H7965", "H2617"] + for num in common: + response = client.get(f"/strongs/{num}") + assert response.status_code == 200, f"Failed for {num}" + + def test_common_greek_words(self, client): + """Test common Greek Strong's numbers""" + common = ["G26", "G2316", "G3056", "G4102", "G5485"] + for num in common: + response = client.get(f"/strongs/{num}") + assert response.status_code == 200, f"Failed for {num}" + + def test_strongs_with_derivation_links(self, client): + """Test entry with derivation containing Strong's references""" + # H1580 has derivation with references to other Strong's numbers + response = client.get("/strongs/H1580") + assert response.status_code == 200 + content = response.content.decode() + # Should have linked Strong's references in derivation + assert "/strongs/H" in content or "derivation" in content.lower() + + +class TestStrongsIntegration: + """Integration tests for Strong's functionality""" + + def test_search_then_view_entry(self, client): + """Test searching and then viewing an entry""" + # Search for love + search_response = client.get("/strongs?q=love") + assert search_response.status_code == 200 + + # View G26 (agape) + entry_response = client.get("/strongs/G26") + assert entry_response.status_code == 200 + content = entry_response.content.decode() + assert "love" in content.lower() + + def test_browse_hebrew_then_entry(self, client): + """Test browsing Hebrew index then viewing entry""" + # Browse Hebrew + index_response = client.get("/strongs/hebrew") + assert index_response.status_code == 200 + + # View first entry + entry_response = client.get("/strongs/H1") + assert entry_response.status_code == 200 + + def test_navigate_between_entries(self, client): + """Test navigation between Strong's entries""" + response = client.get("/strongs/H100") + assert response.status_code == 200 + content = response.content.decode() + + # Should have navigation to adjacent entries + assert "H99" in content or "H101" in content + + def test_strongs_page_has_breadcrumbs(self, client): + """Test Strong's pages have breadcrumb navigation""" + response = client.get("/strongs/G26") + assert response.status_code == 200 + content = response.content.decode() + + # Should have link back to Strong's index + assert "/strongs" in content