This is bold text with italics and links.
", - type=SegmentType.CONTENT - ) - - with patch("pecha_api.sheets.sheets_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, return_value=mock_table_of_content), \ - patch("pecha_api.sheets.sheets_service.get_sheet_first_content_by_ids", new_callable=AsyncMock, return_value=mock_content_segment): - - result = await _generate_sheet_summary_(sheet_id) - - expected = "This is bold text with italics and links." - assert result == expected - - -@pytest.mark.asyncio -async def test_generate_sheet_summary_exceeds_max_words(): - #Test content that exceeds max_words limit - sheet_id = "test_sheet_id" - - # Create content with more than 30 words (default max_words) - long_content = " ".join([f"word{i}" for i in range(1, 41)]) # 40 words - - mock_table_of_content = TableOfContent( - text_id=sheet_id, - type=TableOfContentType.SHEET, - sections=[ - Section( - id="section_1", - section_number=1, - segments=[ - TextSegment(segment_id="content_segment_1", segment_number=1) - ] - ) - ] - ) - - mock_content_segment = SegmentDTO( - id="content_segment_1", - text_id=sheet_id, - content=long_content, - type=SegmentType.CONTENT - ) - - with patch("pecha_api.sheets.sheets_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, return_value=mock_table_of_content), \ - patch("pecha_api.sheets.sheets_service.get_sheet_first_content_by_ids", new_callable=AsyncMock, return_value=mock_content_segment): - - result = await _generate_sheet_summary_(sheet_id) - - # Should contain exactly 30 words plus "..." - words = result.split() - assert len(words) == 30 # 30 words (the last word has "..." appended) - assert result.endswith("...") - assert "word1" in result - assert "word30" in result - assert "word31" not in result.replace("...", "") - - -@pytest.mark.asyncio -async def test_generate_sheet_summary_custom_max_words(): - #Test with limited content (less than default 30 words) - sheet_id = "test_sheet_id" - - mock_table_of_content = TableOfContent( - text_id=sheet_id, - type=TableOfContentType.SHEET, - sections=[ - Section( - id="section_1", - section_number=1, - segments=[ - TextSegment(segment_id="content_segment_1", segment_number=1) - ] - ) - ] - ) - - mock_content_segment = SegmentDTO( - id="content_segment_1", - text_id=sheet_id, - content="This is a test content with only a few words.", - type=SegmentType.CONTENT - ) - - with patch("pecha_api.sheets.sheets_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, return_value=mock_table_of_content), \ - patch("pecha_api.sheets.sheets_service.get_sheet_first_content_by_ids", new_callable=AsyncMock, return_value=mock_content_segment): - - result = await _generate_sheet_summary_(sheet_id) - - expected = "This is a test content with only a few words." - assert result == expected - - -@pytest.mark.asyncio -async def test_generate_sheet_summary_no_table_of_content(): - #Test when table of content doesn't exist - sheet_id = "test_sheet_id" - - with patch("pecha_api.sheets.sheets_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, return_value=None): - - result = await _generate_sheet_summary_(sheet_id) - - assert result == "" - - -@pytest.mark.asyncio -async def test_generate_sheet_summary_empty_sections(): - #Test when table of content exists but has no sections - sheet_id = "test_sheet_id" - - mock_table_of_content = TableOfContent( - text_id=sheet_id, - type=TableOfContentType.SHEET, - sections=[] - ) - - with patch("pecha_api.sheets.sheets_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, return_value=mock_table_of_content): - - result = await _generate_sheet_summary_(sheet_id) - - assert result == "" - - -@pytest.mark.asyncio -async def test_generate_sheet_summary_no_segments(): - #Test when no segments exist in the table of content - sheet_id = "test_sheet_id" - - mock_table_of_content = TableOfContent( - text_id=sheet_id, - type=TableOfContentType.SHEET, - sections=[ - Section( - id="section_1", - section_number=1, - segments=[] - ) - ] - ) - - with patch("pecha_api.sheets.sheets_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, return_value=mock_table_of_content): - - result = await _generate_sheet_summary_(sheet_id) - - assert result == "" - - -@pytest.mark.asyncio -async def test_generate_sheet_summary_no_content_segments(): - #Test when no content type segments exist (only images, sources, etc.) - sheet_id = "test_sheet_id" - - mock_table_of_content = TableOfContent( - text_id=sheet_id, - type=TableOfContentType.SHEET, - sections=[ - Section( - id="section_1", - section_number=1, - segments=[ - TextSegment(segment_id="image_segment_1", segment_number=1), - TextSegment(segment_id="source_segment_1", segment_number=2) - ] - ) - ] - ) - - with patch("pecha_api.sheets.sheets_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, return_value=mock_table_of_content), \ - patch("pecha_api.sheets.sheets_service.get_sheet_first_content_by_ids", new_callable=AsyncMock, return_value=None): - - result = await _generate_sheet_summary_(sheet_id) - - assert result == "" - - -@pytest.mark.asyncio -async def test_generate_sheet_summary_empty_content(): - #Test when content segments exist but have empty content - sheet_id = "test_sheet_id" - - mock_table_of_content = TableOfContent( - text_id=sheet_id, - type=TableOfContentType.SHEET, - sections=[ - Section( - id="section_1", - section_number=1, - segments=[ - TextSegment(segment_id="content_segment_1", segment_number=1), - TextSegment(segment_id="content_segment_2", segment_number=2) - ] - ) - ] - ) - - mock_content_segment = SegmentDTO( - id="content_segment_1", - text_id=sheet_id, - content="", # Empty string instead of None - type=SegmentType.CONTENT - ) - - with patch("pecha_api.sheets.sheets_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, return_value=mock_table_of_content), \ - patch("pecha_api.sheets.sheets_service.get_sheet_first_content_by_ids", new_callable=AsyncMock, return_value=mock_content_segment): - - result = await _generate_sheet_summary_(sheet_id) - - assert result == "" - - -@pytest.mark.asyncio -async def test_generate_sheet_summary_error_handling(): - #Test error handling - should return empty string when exceptions occurd - sheet_id = "test_sheet_id" - - # Simulate an exception in get_table_of_content_by_sheet_id - with patch("pecha_api.sheets.sheets_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, side_effect=Exception("Database error")): - - result = await _generate_sheet_summary_(sheet_id) - - assert result == "" - - -@pytest.mark.asyncio -async def test_generate_sheet_summary_segment_not_found(): - #Test when segment IDs in table of content don't exist in segments dictionaryx1 - sheet_id = "test_sheet_id" - - mock_table_of_content = TableOfContent( - text_id=sheet_id, - type=TableOfContentType.SHEET, - sections=[ - Section( - id="section_1", - section_number=1, - segments=[ - TextSegment(segment_id="missing_segment", segment_number=1) - ] - ) - ] - ) - - with patch("pecha_api.sheets.sheets_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, return_value=mock_table_of_content), \ - patch("pecha_api.sheets.sheets_service.get_sheet_first_content_by_ids", new_callable=AsyncMock, return_value=None): - - result = await _generate_sheet_summary_(sheet_id) - - assert result == "" - -# Test cases for _strip_html_tags_ function -def test_strip_html_tags_simple_tags(): - #Test stripping simple HTML tags# - html_content = "Hello world!
" - result = _strip_html_tags_(html_content) - assert result == "Hello world!" - -def test_strip_html_tags_complex_html(): - #Test stripping complex HTML with attributes# - html_content = 'Test link content
Content with spaces
" - result = _strip_html_tags_(html_content) - assert result == "Content with spaces" - -def test_strip_html_tags_nested_tags(): - #Test stripping nested HTML tags# - html_content = "Nested content
This is bold text with italics.
" - result = clean_text(content) - assert result == "This is bold text with italics." - -def test_clean_text_empty_content(): - #Test clean_text with empty content# - result = clean_text("") - assert result == "" - -def test_clean_text_whitespace_only(): - #Test clean_text with whitespace only content# - result = clean_text(" ") - assert result == "" - - -# Test cases for _delete_sheet_table_of_content_cache_ function -@pytest.mark.asyncio -async def test_delete_sheet_table_of_content_cache_success(): - #Test _delete_sheet_table_of_content_cache_ with valid table of content# - sheet_id = "test_sheet_id" - mock_table_of_content = TableOfContent( - text_id=sheet_id, - type=TableOfContentType.SHEET, - sections=[] - ) - - with patch("pecha_api.sheets.sheets_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, return_value=mock_table_of_content), \ - patch("pecha_api.sheets.sheets_service.delete_table_of_content_by_sheet_id_cache", new_callable=AsyncMock) as mock_delete_cache: - - from pecha_api.sheets.sheets_service import _delete_sheet_table_of_content_cache_ - result = await _delete_sheet_table_of_content_cache_(sheet_id=sheet_id) - - assert result == mock_table_of_content - mock_delete_cache.assert_called_once() - -@pytest.mark.asyncio -async def test_delete_sheet_table_of_content_cache_no_content(): - #Test _delete_sheet_table_of_content_cache_ with no table of content# - sheet_id = "test_sheet_id" - - with patch("pecha_api.sheets.sheets_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, return_value=None), \ - patch("pecha_api.sheets.sheets_service.delete_table_of_content_by_sheet_id_cache", new_callable=AsyncMock) as mock_delete_cache: - - from pecha_api.sheets.sheets_service import _delete_sheet_table_of_content_cache_ - result = await _delete_sheet_table_of_content_cache_(sheet_id=sheet_id) - - assert result is None - mock_delete_cache.assert_not_called() - - -# Test cases for _delete_sheet_segments_cache_ function -@pytest.mark.asyncio -async def test_delete_sheet_segments_cache_with_content(): - #Test _delete_sheet_segments_cache_ with valid table of content# - mock_table_of_content = TableOfContent( - text_id="test_id", - type=TableOfContentType.SHEET, - sections=[ - Section( - id="section_1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1), - TextSegment(segment_id="seg_2", segment_number=2) - ] - ) - ] - ) - - with patch("pecha_api.sheets.sheets_service.delete_segments_details_by_ids_cache", new_callable=AsyncMock) as mock_delete_cache: - from pecha_api.sheets.sheets_service import _delete_sheet_segments_cache_ - await _delete_sheet_segments_cache_(sheet_table_of_content=mock_table_of_content) - - mock_delete_cache.assert_called_once() - call_args = mock_delete_cache.call_args - assert "seg_1" in call_args[1]['segment_ids'] - assert "seg_2" in call_args[1]['segment_ids'] - -@pytest.mark.asyncio -async def test_delete_sheet_segments_cache_no_content(): - #Test _delete_sheet_segments_cache_ with None table of content# - with patch("pecha_api.sheets.sheets_service.delete_segments_details_by_ids_cache", new_callable=AsyncMock) as mock_delete_cache: - from pecha_api.sheets.sheets_service import _delete_sheet_segments_cache_ - await _delete_sheet_segments_cache_(sheet_table_of_content=None) - - mock_delete_cache.assert_called_once() - call_args = mock_delete_cache.call_args - assert call_args[1]['segment_ids'] == [] - - -# Test cases for delete_sheet_by_id unauthorized access -@pytest.mark.asyncio -async def test_delete_sheet_forbidden_access(): - #Test delete_sheet_by_id when user tries to delete another user's sheet# - sheet_id = "test_sheet_id" - token = "valid_token" - - text_details = TextDTO( - id=sheet_id, - title="sheet_title", - language="language", - group_id="group_id", - type=TextType.SHEET, - is_published=True, - created_date="2021-01-01", - updated_date="2021-01-01", - published_date="2021-01-01", - published_by="owner@gmail.com", # Different user - categories=[], - views=10 - ) - - current_user_details = UserInfoResponse( - firstname="Current", - lastname="User", - username="currentuser", - email="current@gmail.com", # Different email - educations=[], - followers=0, - following=0, - social_profiles=[] - ) - - with patch("pecha_api.sheets.sheets_service.validate_user_exists", return_value=True), \ - patch("pecha_api.sheets.sheets_service.TextUtils.get_text_details_by_id", new_callable=AsyncMock, return_value=text_details), \ - patch("pecha_api.sheets.sheets_service.get_user_info", new_callable=AsyncMock, return_value=current_user_details): - - with pytest.raises(HTTPException) as exc_info: - await delete_sheet_by_id(sheet_id=sheet_id, token=token) - - assert exc_info.value.status_code == status.HTTP_403_FORBIDDEN - assert exc_info.value.detail == ErrorConstants.FORBIDDEN_ERROR_MESSAGE - - -# Test cases for _get_sheet_first_content_details_by_type_ function -@pytest.mark.asyncio -async def test_get_sheet_first_content_details_by_type_success(): - #Test _get_sheet_first_content_details_by_type_ with valid segment IDs# - segment_ids = ["seg_1", "seg_2", "seg_3"] - mock_segment = SegmentDTO( - id="seg_1", - text_id="text_id", - content="first content", - type=SegmentType.CONTENT - ) - - with patch("pecha_api.sheets.sheets_service.get_sheet_first_content_by_ids", new_callable=AsyncMock, return_value=mock_segment): - from pecha_api.sheets.sheets_service import _get_sheet_first_content_details_by_type_ - result = await _get_sheet_first_content_details_by_type_( - segment_ids=segment_ids, - segment_type=SegmentType.CONTENT - ) - - assert result == mock_segment - -@pytest.mark.asyncio -async def test_get_sheet_first_content_details_by_type_no_result(): - #Test _get_sheet_first_content_details_by_type_ with no matching segments# - segment_ids = ["seg_1", "seg_2", "seg_3"] - - with patch("pecha_api.sheets.sheets_service.get_sheet_first_content_by_ids", new_callable=AsyncMock, return_value=None): - from pecha_api.sheets.sheets_service import _get_sheet_first_content_details_by_type_ - result = await _get_sheet_first_content_details_by_type_( - segment_ids=segment_ids, - segment_type=SegmentType.CONTENT - ) - - assert result is None - - -# Test cases for _get_all_segment_ids_in_table_of_content_ edge cases -def test_get_all_segment_ids_empty_segments(): - #Test _get_all_segment_ids_in_table_of_content_ with sections containing no segments# - sheet_sections = [ - Section( - id="section_1", - section_number=1, - segments=[] - ), - Section( - id="section_2", - section_number=2, - segments=[] - ) - ] - - result = _get_all_segment_ids_in_table_of_content_(sheet_sections=sheet_sections) - assert result == [] - -def test_get_all_segment_ids_multiple_sections(): - #Test _get_all_segment_ids_in_table_of_content_ with multiple sections# - sheet_sections = [ - Section( - id="section_1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1), - TextSegment(segment_id="seg_2", segment_number=2) - ] - ), - Section( - id="section_2", - section_number=2, - segments=[ - TextSegment(segment_id="seg_3", segment_number=1), - TextSegment(segment_id="seg_4", segment_number=2) - ] - ) - ] - - result = _get_all_segment_ids_in_table_of_content_(sheet_sections=sheet_sections) - assert result == ["seg_1", "seg_2", "seg_3", "seg_4"] - - -# Test cases for fetch_sheets with specific parameters -@pytest.mark.asyncio -async def test_fetch_sheets_with_sort_parameters(): - #Test fetch_sheets with sorting parameters# - mock_user = UserInfoResponse( - firstname="Test", - lastname="User", - username="testuser", - email="test@example.com", - educations=[], - followers=0, - following=0, - social_profiles=[] - ) - mock_sheets = _generate_mock_sheets_response_() - - with patch("pecha_api.sheets.sheets_service.get_sheet", new_callable=AsyncMock, return_value=mock_sheets) as mock_get_sheet, \ - patch("pecha_api.sheets.sheets_service.Utils.time_passed", return_value="1 day ago"), \ - patch("pecha_api.sheets.sheets_service.fetch_user_by_email", return_value=mock_user), \ - patch("pecha_api.texts.texts_models.Text.get_published_sheets_count_from_db", new_callable=AsyncMock, return_value=5): - - result = await fetch_sheets( - token="valid_token", - language="en", - email=None, - sort_by=SortBy.CREATED_DATE, - sort_order=SortOrder.ASC, - skip=5, - limit=20 - ) - - # Verify get_sheet was called with correct parameters - mock_get_sheet.assert_called_once_with( - is_published=True, - sort_by=SortBy.CREATED_DATE, - sort_order=SortOrder.ASC, - skip=5, - limit=20 - ) - - assert isinstance(result, SheetDTOResponse) - assert result.skip == 5 - assert result.limit == 20 - - -# Additional test for upload_sheet_image_request error handling -def test_upload_sheet_image_request_validation_error(): - #Test upload_sheet_image_request when image validation fails# - file_content = io.BytesIO(b"invalid_image_data") - mock_file = MagicMock(spec=UploadFile) - mock_file.filename = "test.jpg" - mock_file.file = file_content - mock_file.content_type = "image/jpeg" - - with patch("pecha_api.sheets.sheets_service.ImageUtils.validate_and_compress_image", side_effect=HTTPException(status_code=400, detail="Invalid image")): - with pytest.raises(HTTPException) as exc_info: - upload_sheet_image_request(sheet_id="test_id", file=mock_file) - - assert exc_info.value.status_code == 400 - assert exc_info.value.detail == "Invalid image" - - -# Test case for _generate_sheet_detail_dto_ with different publisher name scenarios -@pytest.mark.asyncio -async def test_generate_sheet_detail_dto_empty_publisher_name(): - #Test _generate_sheet_detail_dto_ with empty publisher first/last name# - sheet_details = TextDTO( - id="sheet_id", - title="Test Sheet", - language="en", - group_id="group_id", - type=TextType.SHEET, - is_published=True, - created_date="2021-01-01", - updated_date="2021-01-01", - published_date="2021-01-01", - published_by="test@example.com", - categories=[], - views=50 - ) - - user_details = UserInfoResponse( - firstname="", # Empty first name - lastname="", # Empty last name - username="testuser", - email="test@example.com", - educations=[], - followers=0, - following=0, - social_profiles=[] - ) - - sheet_sections = [] - - with patch("pecha_api.sheets.sheets_service.get_segments_details_by_ids", new_callable=AsyncMock, return_value={}): - result = await _generate_sheet_detail_dto_( - sheet_details=sheet_details, - user_details=user_details, - sheet_sections=sheet_sections, - skip=0, - limit=10 - ) - - assert isinstance(result, SheetDetailDTO) - assert result.id == "sheet_id" - assert result.publisher.name == " " # Empty first and last name results in a space character - - -# Test case for _generate_sheet_section_ with SOURCE segment error handling -@pytest.mark.asyncio -async def test_generate_sheet_section_source_segment_error(): - #Test _generate_sheet_section_ when source text details cannot be retrieved# - segments = [ - TextSegment(segment_id="source_segment", segment_number=1) - ] - - segments_dict = { - "source_segment": SegmentDTO( - id="source_segment", - text_id="invalid_text_id", - content="source_content", - type=SegmentType.SOURCE - ) - } - - with patch("pecha_api.sheets.sheets_service.TextUtils.get_text_details_by_id", new_callable=AsyncMock, side_effect=HTTPException(status_code=404, detail="Text not found")): - - with pytest.raises(HTTPException): - await _generate_sheet_section_(segments=segments, segments_dict=segments_dict) - - -# Test case for create_new_sheet with invalid token in internal function -@pytest.mark.asyncio -async def test_create_new_sheet_group_creation_error(): - #Test create_new_sheet when group creation fails# - mock_create_sheet_request = CreateSheetRequest( - title="Test Sheet", - source=[], - is_published=True - ) - - with patch("pecha_api.sheets.sheets_service.create_new_group", new_callable=AsyncMock, side_effect=HTTPException(status_code=401, detail="Unauthorized")): - - with pytest.raises(HTTPException) as exc_info: - await create_new_sheet( - create_sheet_request=mock_create_sheet_request, - token="invalid_token" - ) - - assert exc_info.value.status_code == 401 - - -# Test case for update_sheet_by_id cache deletion error handling -@pytest.mark.asyncio -async def test_update_sheet_cache_deletion_error(): - #Test update_sheet_by_id when cache deletion fails - should propagate the error# - sheet_id = str(uuid.uuid4()) - mock_update_request = CreateSheetRequest( - title="Updated Title", - source=[ - Source(position=1, type=SegmentType.CONTENT, content="test content") - ], - is_published=True - ) - - with patch("pecha_api.sheets.sheets_service.validate_user_exists", return_value=True), \ - patch("pecha_api.sheets.sheets_service.delete_text_details_by_id_cache", new_callable=AsyncMock, side_effect=Exception("Cache error")): - - # Should propagate the cache error since there's no error handling in the function - with pytest.raises(Exception) as exc_info: - await update_sheet_by_id( - sheet_id=sheet_id, - update_sheet_request=mock_update_request, - token="valid_token" - ) - - assert str(exc_info.value) == "Cache error" - - -# Test case for _generate_segment_dictionary_ with mixed segment types -def test_generate_segment_dictionary_mixed_types(): - #Test _generate_segment_dictionary_ with mixed segment types including SOURCE# - segments_response = SegmentResponse( - segments=[ - SegmentDTO( - id="content_seg", - text_id="text_id", - content="content text", - type=SegmentType.CONTENT - ), - SegmentDTO( - id="source_seg", - text_id="text_id", - content="source_id", - type=SegmentType.SOURCE - ), - SegmentDTO( - id="image_seg", - text_id="text_id", - content="image_url", - type=SegmentType.IMAGE - ) - ] - ) - - result = _generate_segment_dictionary_(new_segments=segments_response) - - # Should only contain non-SOURCE segments - content_hash = hashlib.sha256("content text".encode()).hexdigest() - image_hash = hashlib.sha256("image_url".encode()).hexdigest() - source_hash = hashlib.sha256("source_id".encode()).hexdigest() - - assert content_hash in result - assert image_hash in result - assert source_hash not in result - assert result[content_hash] == "content_seg" - assert result[image_hash] == "image_seg" - - -# Test case for _generate_segment_creation_request_payload_ with only SOURCE segments -def test_generate_segment_creation_request_payload_only_sources(): - #Test _generate_segment_creation_request_payload_ with only SOURCE type segments# - create_request = CreateSheetRequest( - title="Test Sheet", - source=[ - Source(position=1, type=SegmentType.SOURCE, content="source_id_1"), - Source(position=2, type=SegmentType.SOURCE, content="source_id_2") - ], - is_published=True - ) - - result = _generate_segment_creation_request_payload_( - create_sheet_request=create_request, - text_id="test_text_id" - ) - - assert isinstance(result, CreateSegmentRequest) - assert result.text_id == "test_text_id" - assert len(result.segments) == 0 # Should exclude all SOURCE segments - - -# Test case for _create_sheet_text_ with invalid token -@pytest.mark.asyncio -async def test_create_sheet_text_invalid_token(): - #Test _create_sheet_text_ with invalid token# - with patch("pecha_api.sheets.sheets_service.validate_and_extract_user_details", side_effect=HTTPException(status_code=401, detail="Invalid token")): - - with pytest.raises(HTTPException) as exc_info: - await _create_sheet_text_( - title="Test Sheet", - token="invalid_token", - group_id="group_id" - ) - - assert exc_info.value.status_code == 401 - - -# Test case for _create_sheet_group_ with invalid token -@pytest.mark.asyncio -async def test_create_sheet_group_invalid_token(): - #Test _create_sheet_group_ with invalid token# - with patch("pecha_api.sheets.sheets_service.create_new_group", new_callable=AsyncMock, side_effect=HTTPException(status_code=401, detail="Invalid token")): - - with pytest.raises(HTTPException) as exc_info: - await _create_sheet_group_(token="invalid_token") - - assert exc_info.value.status_code == 401 \ No newline at end of file diff --git a/tests/sheets/test_sheets_views.py b/tests/sheets/test_sheets_views.py deleted file mode 100644 index b0884998a..000000000 --- a/tests/sheets/test_sheets_views.py +++ /dev/null @@ -1,149 +0,0 @@ -import pytest -from unittest.mock import patch, AsyncMock - - -from pecha_api.sheets.sheets_views import ( - create_sheet, - get_sheet -) -from pecha_api.sheets.sheets_response_models import ( - CreateSheetRequest, - SheetDetailDTO, - SheetSection, - Publisher -) - -from pecha_api.texts.texts_response_models import TableOfContent, TableOfContentType, Section, TextSegment - -@pytest.mark.asyncio -async def test_create_sheet_success(): - mock_source = [] - mock_create_sheet_request = CreateSheetRequest( - title="sheet_title", - source=mock_source - ) - - mock_table_of_content_response = TableOfContent( - id="table_of_content_id", - text_id="text_id", - type=TableOfContentType.SHEET, - sections=[ - Section( - id="section_id", - section_number=1, - segments=[ - TextSegment( - segment_id="segment_id_1", - segment_number=1 - ), - TextSegment( - segment_id="segment_id_2", - segment_number=2 - ), - TextSegment( - segment_id="segment_id_3", - segment_number=3 - ) - ] - ) - ] - ) - - with patch("pecha_api.sheets.sheets_views.create_new_sheet", new_callable=AsyncMock, return_value=mock_table_of_content_response): - - response = await create_sheet( - create_sheet_request=mock_create_sheet_request, - authentication_credential=type("HTTPAuthorizationCredentials", (), {"credentials": "valid_token"})() - ) - - assert response is not None - assert isinstance(response, TableOfContent) - assert response.id == "table_of_content_id" - assert response.text_id == "text_id" - - -@pytest.mark.asyncio -async def test_get_sheet_success(): - sheet_id = "test_sheet_id" - skip = 0 - limit = 10 - - mock_sheet_detail = SheetDetailDTO( - id=sheet_id, - sheet_title="Test Sheet Title", - created_date="2024-01-01T00:00:00Z", - publisher=Publisher( - name="Test User", - username="testuser", - email="test@example.com" - ), - content=SheetSection( - section_number=1, - segments=[] - ), - views=0, - is_published=True, - skip=skip, - limit=limit, - total=1 - ) - - with patch("pecha_api.sheets.sheets_views.get_sheet_by_id", new_callable=AsyncMock, return_value=mock_sheet_detail): - response = await get_sheet( - sheet_id=sheet_id, - skip=skip, - limit=limit - ) - - assert response is not None - assert isinstance(response, SheetDetailDTO) - assert response.id == sheet_id - assert response.sheet_title == "Test Sheet Title" - assert response.publisher.name == "Test User" - assert response.publisher.email == "test@example.com" - assert response.skip == skip - assert response.limit == limit - assert response.total == 1 - - -@pytest.mark.asyncio -async def test_get_sheet_with_custom_pagination(): - sheet_id = "test_sheet_id" - skip = 5 - limit = 20 - - mock_sheet_detail = SheetDetailDTO( - id=sheet_id, - sheet_title="Test Sheet Title", - created_date="2024-01-01T00:00:00Z", - publisher=Publisher( - name="Test User", - username="testuser", - email="test@example.com" - ), - content=SheetSection( - section_number=1, - segments=[] - ), - views=0, - is_published=True, - skip=skip, - limit=limit, - total=1 - ) - - with patch("pecha_api.sheets.sheets_views.get_sheet_by_id", new_callable=AsyncMock, return_value=mock_sheet_detail) as mock_service: - response = await get_sheet( - sheet_id=sheet_id, - skip=skip, - limit=limit - ) - - # Verify the service function was called with correct parameters - mock_service.assert_called_once_with(sheet_id=sheet_id, skip=skip, limit=limit) - - assert response is not None - assert isinstance(response, SheetDetailDTO) - assert response.id == sheet_id - assert response.skip == skip - assert response.limit == limit \ No newline at end of file diff --git a/tests/text_uploader/__init__.py b/tests/text_uploader/__init__.py deleted file mode 100644 index a83913065..000000000 --- a/tests/text_uploader/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Tests for text uploader module.""" - - - - - diff --git a/tests/text_uploader/collections/__init__.py b/tests/text_uploader/collections/__init__.py deleted file mode 100644 index 869be8990..000000000 --- a/tests/text_uploader/collections/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Collection-related tests for text uploader.""" - - - - - diff --git a/tests/text_uploader/collections/test_collection_service.py b/tests/text_uploader/collections/test_collection_service.py deleted file mode 100644 index dac7550da..000000000 --- a/tests/text_uploader/collections/test_collection_service.py +++ /dev/null @@ -1,701 +0,0 @@ -import pytest -from unittest.mock import AsyncMock, patch - -from pecha_api.text_uploader.collections.collection_service import CollectionService - - -def test_build_multilingual_payload_merges_languages_and_sets_slug_from_en_title(): - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [ - { - "_id": {"$oid": "c1"}, - "title": "Liturgy", - "description": "Prayers", - "parent_id": None, - "has_sub_child": False, - } - ], - }, - { - "language": "bo", - "collections": [ - { - "_id": {"$oid": "c1"}, - "title": "ཁ་འདོན།", - "description": "ཆོ་ག", - "parent_id": None, - "has_sub_child": False, - } - ], - }, - ] - - payloads = service.build_multilingual_payload(collections_by_language) - assert len(payloads) == 1 - - payload = payloads[0] - assert payload["pecha_collection_id"] == "c1" - assert payload["slug"] == "Liturgy" - assert payload["titles"]["en"] == "Liturgy" - assert payload["titles"]["bo"] == "ཁ་འདོན།" - assert payload["descriptions"]["en"] == "Prayers" - assert payload["descriptions"]["bo"] == "ཆོ་ག" - # configured languages should always exist as keys - assert "zh" in payload["descriptions"] - - -@pytest.mark.asyncio -async def test_build_recursive_multilingual_payloads_recurse_children_uploads_all_levels(): - service = CollectionService() - - root_level = [ - { - "language": "en", - "collections": [ - { - "_id": {"$oid": "c1"}, - "title": "Root", - "description": "Root desc", - "parent_id": None, - "has_sub_child": True, - } - ], - }, - {"language": "bo", "collections": [{"_id": {"$oid": "c1"}, "title": "རྩ་བ།"}]}, - {"language": "zh", "collections": [{"_id": {"$oid": "c1"}, "title": "根"}]}, - ] - child_level = [ - { - "language": "en", - "collections": [ - { - "_id": {"$oid": "c2"}, - "title": "Child", - "description": "Child desc", - "parent_id": {"$oid": "c1"}, - "has_sub_child": False, - } - ], - }, - {"language": "bo", "collections": [{"_id": {"$oid": "c2"}, "title": "བུ།"}]}, - {"language": "zh", "collections": [{"_id": {"$oid": "c2"}, "title": "子"}]}, - ] - - async def fake_get_collections_service(*, openpecha_api_url: str, parent_id=None): - if parent_id is None: - return root_level - if parent_id == "c1": - return child_level - return [{"language": "en", "collections": []}] - - with patch.object( - service, - "get_collections_service", - new=AsyncMock(side_effect=fake_get_collections_service), - ), patch( - "pecha_api.text_uploader.collections.collection_service.get_collection_by_pecha_collection_id", - new_callable=AsyncMock, - return_value=None, - ), patch( - "pecha_api.text_uploader.collections.collection_service.post_collections", - new_callable=AsyncMock, - side_effect=[ - {"id": {"$oid": "local_c1"}}, - {"id": "local_c2"}, - ], - ) as mock_post: - payloads = await service.build_recursive_multilingual_payloads( - destination_url="https://dest.example/api/v1", - openpecha_api_url="https://openpecha.example", - access_token="tok", - ) - - assert mock_post.await_count == 2 - - assert len(payloads) == 1 - root_payload = payloads[0] - assert root_payload["pecha_collection_id"] == "c1" - assert root_payload["local_id"] == "local_c1" - - assert "children" in root_payload - assert len(root_payload["children"]) == 1 - child_payload = root_payload["children"][0] - assert child_payload["pecha_collection_id"] == "c2" - assert child_payload["local_id"] == "local_c2" - - # Ensure recursion used the root's local_id as the child's parent_id in POST. - first_call = mock_post.await_args_list[0] - second_call = mock_post.await_args_list[1] - assert first_call.kwargs["collection_model"].parent_id is None - assert second_call.kwargs["collection_model"].parent_id == "local_c1" - -import pytest -from unittest.mock import AsyncMock, patch, MagicMock - -from pecha_api.text_uploader.collections.collection_service import CollectionService -from pecha_api.text_uploader.constants import COLLECTION_LANGUAGES -from pecha_api.text_uploader.collections.collection_model import CollectionPayload -from pecha_api.text_uploader.text_uploader_response_model import TextUploadRequest - - -@pytest.mark.asyncio -@pytest.mark.parametrize("parent_id", [None, "parent_1"]) -async def test_get_collections_service_delegates_to_repository(parent_id: str | None): - expected = [{"language": "en", "collections": [{"id": "c1"}]}] - - with patch( - "pecha_api.text_uploader.collections.collection_service.get_collections", - new_callable=AsyncMock, - return_value=expected, - ) as mock_get_collections: - service = CollectionService() - - result = await service.get_collections_service( - openpecha_api_url="https://openpecha.example", - parent_id=parent_id, - ) - - assert result == expected - mock_get_collections.assert_awaited_once_with( - openpecha_api_url="https://openpecha.example", - languages=COLLECTION_LANGUAGES, - parent_id=parent_id, - ) - - -@pytest.mark.asyncio -async def test_upload_collections_success(): - """Test upload_collections calls build_recursive_multilingual_payloads""" - service = CollectionService() - - text_upload_request = TextUploadRequest( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - text_id="test_text_id" - ) - token = "test_token" - - with patch.object( - service, - "build_recursive_multilingual_payloads", - new_callable=AsyncMock - ) as mock_build: - await service.upload_collections( - text_upload_request=text_upload_request, - token=token - ) - - mock_build.assert_awaited_once_with( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - access_token=token - ) - - -@pytest.mark.asyncio -async def test_build_multilingual_payload_single_language(): - """Test build_multilingual_payload with single language collections""" - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [ - { - "id": "collection_id_1", - "slug": "test-collection", - "title": "Test Collection", - "description": "Test Description", - "parent_id": None, - "has_sub_child": False - } - ] - } - ] - - result = service.build_multilingual_payload(collections_by_language) - - assert len(result) == 1 - assert result[0]["pecha_collection_id"] == "collection_id_1" - assert result[0]["slug"] == "Test Collection" - assert result[0]["titles"] == {"en": "Test Collection"} - assert result[0]["descriptions"]["en"] == "Test Description" - assert result[0]["parent_id"] is None - assert result[0]["has_sub_child"] is False - - -@pytest.mark.asyncio -async def test_build_multilingual_payload_multiple_languages(): - """Test build_multilingual_payload with multiple languages for same collection""" - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [ - { - "id": "collection_id_1", - "slug": "madhyamaka", - "title": "Madhyamaka", - "description": "Madhyamaka treatises", - "parent_id": None, - "has_sub_child": True - } - ] - }, - { - "language": "bo", - "collections": [ - { - "id": "collection_id_1", - "slug": "madhyamaka", - "title": "དབུ་མ།", - "description": "དབུ་མའི་གཞུང་སྣ་ཚོགས།", - "parent_id": None, - "has_sub_child": True - } - ] - } - ] - - result = service.build_multilingual_payload(collections_by_language) - - assert len(result) == 1 - assert result[0]["pecha_collection_id"] == "collection_id_1" - assert result[0]["slug"] == "Madhyamaka" # English title is used as slug - assert result[0]["titles"] == {"en": "Madhyamaka", "bo": "དབུ་མ།"} - assert result[0]["descriptions"]["en"] == "Madhyamaka treatises" - assert result[0]["descriptions"]["bo"] == "དབུ་མའི་གཞུང་སྣ་ཚོགས།" - assert result[0]["has_sub_child"] is True - - -@pytest.mark.asyncio -async def test_build_multilingual_payload_with_oid_format(): - """Test build_multilingual_payload with MongoDB ObjectId format""" - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [ - { - "_id": {"$oid": "507f1f77bcf86cd799439011"}, - "slug": "test", - "title": "Test Collection", - "description": "Test Description", - "parent_id": {"$oid": "507f1f77bcf86cd799439012"}, - "has_child": True - } - ] - } - ] - - result = service.build_multilingual_payload(collections_by_language) - - assert len(result) == 1 - assert result[0]["pecha_collection_id"] == "507f1f77bcf86cd799439011" - assert result[0]["parent_id"] == {"$oid": "507f1f77bcf86cd799439012"} - assert result[0]["has_sub_child"] is True - - -@pytest.mark.asyncio -async def test_build_multilingual_payload_multiple_collections(): - """Test build_multilingual_payload with multiple different collections""" - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [ - { - "id": "collection_1", - "title": "Collection 1", - "description": "Description 1", - "has_sub_child": False - }, - { - "id": "collection_2", - "title": "Collection 2", - "description": "Description 2", - "has_sub_child": True - } - ] - } - ] - - result = service.build_multilingual_payload(collections_by_language) - - assert len(result) == 2 - collection_ids = [r["pecha_collection_id"] for r in result] - assert "collection_1" in collection_ids - assert "collection_2" in collection_ids - - -@pytest.mark.asyncio -async def test_build_multilingual_payload_with_titles_dict(): - """Test build_multilingual_payload when titles/descriptions are already dicts""" - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [ - { - "id": "collection_1", - "slug": "test", - "titles": {"en": "English Title", "bo": "བོད་ཡིག"}, - "descriptions": {"en": "English Description"}, - "has_sub_child": False - } - ] - } - ] - - result = service.build_multilingual_payload(collections_by_language) - - assert len(result) == 1 - assert result[0]["titles"]["en"] == "English Title" - assert result[0]["slug"] == "English Title" - - -@pytest.mark.asyncio -async def test_build_recursive_multilingual_payloads_no_children(): - """Test build_recursive_multilingual_payloads with collections that have no children""" - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [ - { - "id": "collection_1", - "title": "Test Collection", - "description": "Test Description", - "parent_id": None, - "has_sub_child": False - } - ] - } - ] - - with patch.object( - service, - "get_collections_service", - new_callable=AsyncMock, - return_value=collections_by_language - ) as mock_get_collections, \ - patch( - "pecha_api.text_uploader.collections.collection_service.get_collection_by_pecha_collection_id", - new_callable=AsyncMock, - return_value=None - ) as mock_get_existing, \ - patch( - "pecha_api.text_uploader.collections.collection_service.post_collections", - new_callable=AsyncMock, - return_value={"id": "new_local_id_1"} - ) as mock_post: - result = await service.build_recursive_multilingual_payloads( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - access_token="test_token" - ) - - assert len(result) == 1 - assert result[0]["pecha_collection_id"] == "collection_1" - assert result[0]["children"] == [] - assert result[0]["local_id"] == "new_local_id_1" - mock_post.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_build_recursive_multilingual_payloads_multiple_collections_no_children(): - """Test build_recursive_multilingual_payloads with multiple collections without children""" - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [ - { - "id": "collection_1", - "title": "Collection 1", - "description": "Description 1", - "has_sub_child": False - }, - { - "id": "collection_2", - "title": "Collection 2", - "description": "Description 2", - "has_sub_child": False - } - ] - } - ] - - with patch.object( - service, - "get_collections_service", - new_callable=AsyncMock, - return_value=collections_by_language - ), \ - patch( - "pecha_api.text_uploader.collections.collection_service.get_collection_by_pecha_collection_id", - new_callable=AsyncMock, - return_value=None - ), \ - patch( - "pecha_api.text_uploader.collections.collection_service.post_collections", - new_callable=AsyncMock, - side_effect=[ - {"id": "local_1"}, - {"id": "local_2"} - ] - ) as mock_post: - result = await service.build_recursive_multilingual_payloads( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - access_token="test_token" - ) - - assert len(result) == 2 - assert result[0]["pecha_collection_id"] == "collection_1" - assert result[0]["local_id"] == "local_1" - assert result[0]["children"] == [] - assert result[1]["pecha_collection_id"] == "collection_2" - assert result[1]["local_id"] == "local_2" - assert result[1]["children"] == [] - assert mock_post.await_count == 2 - - -@pytest.mark.asyncio -async def test_build_recursive_multilingual_payloads_existing_collection(): - """Test build_recursive_multilingual_payloads skips existing collections""" - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [ - { - "id": "existing_collection", - "title": "Existing Collection", - "description": "Already exists", - "has_sub_child": False - } - ] - } - ] - - with patch.object( - service, - "get_collections_service", - new_callable=AsyncMock, - return_value=collections_by_language - ), \ - patch( - "pecha_api.text_uploader.collections.collection_service.get_collection_by_pecha_collection_id", - new_callable=AsyncMock, - return_value="existing_local_id" - ) as mock_get_existing, \ - patch( - "pecha_api.text_uploader.collections.collection_service.post_collections", - new_callable=AsyncMock - ) as mock_post: - result = await service.build_recursive_multilingual_payloads( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - access_token="test_token" - ) - - assert len(result) == 1 - mock_get_existing.assert_awaited_once() - mock_post.assert_not_awaited() - - -@pytest.mark.asyncio -async def test_build_recursive_multilingual_payloads_with_oid_local_id(): - """Test build_recursive_multilingual_payloads handles ObjectId format for local_id""" - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [ - { - "id": "collection_1", - "title": "Test Collection", - "has_sub_child": False - } - ] - } - ] - - with patch.object( - service, - "get_collections_service", - new_callable=AsyncMock, - return_value=collections_by_language - ), \ - patch( - "pecha_api.text_uploader.collections.collection_service.get_collection_by_pecha_collection_id", - new_callable=AsyncMock, - return_value=None - ), \ - patch( - "pecha_api.text_uploader.collections.collection_service.post_collections", - new_callable=AsyncMock, - return_value={"_id": {"$oid": "507f1f77bcf86cd799439011"}} - ): - result = await service.build_recursive_multilingual_payloads( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - access_token="test_token" - ) - - assert len(result) == 1 - assert result[0]["local_id"] == "507f1f77bcf86cd799439011" - - -@pytest.mark.asyncio -async def test_build_recursive_multilingual_payloads_with_local_parent_id(): - """Test build_recursive_multilingual_payloads uses provided local_parent_id""" - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [ - { - "id": "child_collection", - "title": "Child Collection", - "parent_id": "remote_parent_id", - "has_sub_child": False - } - ] - } - ] - - with patch.object( - service, - "get_collections_service", - new_callable=AsyncMock, - return_value=collections_by_language - ), \ - patch( - "pecha_api.text_uploader.collections.collection_service.get_collection_by_pecha_collection_id", - new_callable=AsyncMock, - return_value=None - ), \ - patch( - "pecha_api.text_uploader.collections.collection_service.post_collections", - new_callable=AsyncMock, - return_value={"id": "new_local_id"} - ) as mock_post: - result = await service.build_recursive_multilingual_payloads( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - access_token="test_token", - local_parent_id="local_parent_id_123" - ) - - assert len(result) == 1 - # Verify that the collection was posted with the correct parent_id - call_args = mock_post.call_args - assert call_args[1]["collection_model"].parent_id == "local_parent_id_123" - - -@pytest.mark.asyncio -async def test_build_multilingual_payload_empty_collections(): - """Test build_multilingual_payload with empty collections""" - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [] - } - ] - - result = service.build_multilingual_payload(collections_by_language) - - assert len(result) == 0 - assert result == [] - - -@pytest.mark.asyncio -async def test_build_multilingual_payload_none_descriptions(): - """Test build_multilingual_payload handles None descriptions""" - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [ - { - "id": "collection_1", - "title": "Test Collection", - "description": None, - "has_sub_child": False - } - ] - } - ] - - result = service.build_multilingual_payload(collections_by_language) - - assert len(result) == 1 - assert result[0]["titles"]["en"] == "Test Collection" - # Description for 'en' should be empty string (from initialization) - assert "descriptions" in result[0] - - -@pytest.mark.asyncio -async def test_build_recursive_multilingual_payloads_no_remote_parent_id(): - """Test build_recursive_multilingual_payloads handles missing remote parent id""" - service = CollectionService() - - collections_by_language = [ - { - "language": "en", - "collections": [ - { - "id": "valid_id", # Valid ID but will test edge case - "title": "Test Collection", - "has_sub_child": False # No children to avoid recursion bug - } - ] - } - ] - - with patch.object( - service, - "get_collections_service", - new_callable=AsyncMock, - return_value=collections_by_language - ), \ - patch( - "pecha_api.text_uploader.collections.collection_service.get_collection_by_pecha_collection_id", - new_callable=AsyncMock, - return_value=None - ), \ - patch( - "pecha_api.text_uploader.collections.collection_service.post_collections", - new_callable=AsyncMock, - return_value={"id": "new_id"} - ): - result = await service.build_recursive_multilingual_payloads( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - access_token="test_token" - ) - - assert len(result) == 1 - assert result[0]["pecha_collection_id"] == "valid_id" - # Should have empty children since has_sub_child is False - assert result[0]["children"] == [] - - diff --git a/tests/text_uploader/mapping/test_mapping_service.py b/tests/text_uploader/mapping/test_mapping_service.py deleted file mode 100644 index 0e8e46b9a..000000000 --- a/tests/text_uploader/mapping/test_mapping_service.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest -from unittest.mock import AsyncMock, patch - -from pecha_api.text_uploader.mapping.mapping_services import MappingService -from pecha_api.text_uploader.text_uploader_response_model import TextUploadRequest - - -@pytest.mark.asyncio -async def test_mapping_service_triggers_repo_with_text_ids_and_urls(): - service = MappingService() - request = TextUploadRequest( - destination_url="https://dest.example/api/v1", - openpecha_api_url="https://openpecha.example", - text_id="T1", - ) - - with patch( - "pecha_api.text_uploader.mapping.mapping_services.trigger_mapping_repo", - new_callable=AsyncMock, - ) as mock_trigger: - await service.trigger_mapping( - text_ids={"a": "t1", "b": "t2"}, - text_upload_request=request, - ) - - mock_trigger.assert_awaited_once_with( - ["t1", "t2"], - "https://openpecha.example", - "https://dest.example/api/v1", - ) diff --git a/tests/text_uploader/mapping/test_mapping_services.py b/tests/text_uploader/mapping/test_mapping_services.py deleted file mode 100644 index f9587d3e4..000000000 --- a/tests/text_uploader/mapping/test_mapping_services.py +++ /dev/null @@ -1,31 +0,0 @@ -import pytest -from unittest.mock import AsyncMock, patch - -from pecha_api.text_uploader.mapping.mapping_services import MappingService -from pecha_api.text_uploader.text_uploader_response_model import TextUploadRequest - - -@pytest.mark.asyncio -async def test_trigger_mapping_passes_text_ids_and_urls_to_repo(): - service = MappingService() - request = TextUploadRequest( - destination_url="https://dest.example", - openpecha_api_url="https://openpecha.example", - text_id="t1", - ) - - with patch( - "pecha_api.text_uploader.mapping.mapping_services.trigger_mapping_repo", - new_callable=AsyncMock, - ) as mock_trigger: - await service.trigger_mapping( - text_ids={"a": "i1", "b": "i2"}, - text_upload_request=request, - ) - - mock_trigger.assert_awaited_once_with( - ["i1", "i2"], - "https://openpecha.example", - "https://dest.example", - ) - diff --git a/tests/text_uploader/segments/__init__.py b/tests/text_uploader/segments/__init__.py deleted file mode 100644 index edaedb4e7..000000000 --- a/tests/text_uploader/segments/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Segment-related tests for text uploader.""" - - - - - diff --git a/tests/text_uploader/segments/test_segment_model.py b/tests/text_uploader/segments/test_segment_model.py deleted file mode 100644 index 096c7083f..000000000 --- a/tests/text_uploader/segments/test_segment_model.py +++ /dev/null @@ -1,27 +0,0 @@ -from pecha_api.text_uploader.segments.segment_model import ( - ManifestationModel, - Segment, - SegmentModel, -) - - -def test_segment_models_validate_and_dump(): - model = SegmentModel( - text_id="t1", - segments=[ - Segment(content="a", type="source"), - Segment(content="b", type="source"), - ], - ) - - dumped = model.model_dump() - assert dumped["text_id"] == "t1" - assert dumped["segments"][0] == {"content": "a", "type": "source"} - - -def test_manifestation_model_defaults_none(): - model = ManifestationModel() - assert model.job_id is None - assert model.status is None - assert model.message is None - diff --git a/tests/text_uploader/segments/test_segment_repository.py b/tests/text_uploader/segments/test_segment_repository.py deleted file mode 100644 index 718c52112..000000000 --- a/tests/text_uploader/segments/test_segment_repository.py +++ /dev/null @@ -1,218 +0,0 @@ -import requests -import pytest -from unittest.mock import AsyncMock, patch - - -class DummyResponse: - def __init__( - self, - payload, - *, - ok: bool = True, - status_code: int = 200, - text: str = "", - raise_for_status_exc: Exception | None = None, - ): - self._payload = payload - self.ok = ok - self.status_code = status_code - self.text = text - self._raise_for_status_exc = raise_for_status_exc - - def raise_for_status(self): - if self._raise_for_status_exc: - raise self._raise_for_status_exc - - def json(self): - return self._payload - - -@pytest.mark.asyncio -async def test_get_segments_annotation_calls_instances_endpoint(): - from pecha_api.text_uploader.segments import segment_respository as repo - - response = DummyResponse([{"ok": True}]) - with patch( - "pecha_api.text_uploader.segments.segment_respository.asyncio.to_thread", - new_callable=AsyncMock, - return_value=response, - ) as mock_to_thread: - result = await repo.get_segments_annotation( - pecha_text_id="P1", openpecha_api_url="https://openpecha.example" - ) - - assert result == [{"ok": True}] - assert mock_to_thread.await_count == 1 - call = mock_to_thread.await_args - assert call.args[0] is repo.requests.get - assert call.args[1] == "https://openpecha.example/v2/instances/P1" - assert call.kwargs["params"] == {"annotation": "true", "content": "true"} - - -@pytest.mark.asyncio -async def test_get_segments_id_by_annotation_id_calls_annotations_endpoint(): - from pecha_api.text_uploader.segments import segment_respository as repo - - response = DummyResponse([{"id": "a1"}]) - with patch( - "pecha_api.text_uploader.segments.segment_respository.asyncio.to_thread", - new_callable=AsyncMock, - return_value=response, - ) as mock_to_thread: - result = await repo.get_segments_id_by_annotation_id( - annotation_id="ann_1", openpecha_api_url="https://openpecha.example" - ) - - assert result == [{"id": "a1"}] - call = mock_to_thread.await_args - assert call.args[0] is repo.requests.get - assert call.args[1] == "https://openpecha.example/v2/annotations/ann_1" - -@pytest.mark.asyncio -async def test_get_segments_by_id_calls_annotations_endpoint(): - from pecha_api.text_uploader.segments import segment_respository as repo - - response = DummyResponse({"data": [{"id": "seg"}]}) - with patch( - "pecha_api.text_uploader.segments.segment_respository.asyncio.to_thread", - new_callable=AsyncMock, - return_value=response, - ) as mock_to_thread: - result = await repo.get_segments_by_id( - annotation_id="ann_2", - openpecha_api_url="https://openpecha.example", - ) - - assert result == {"data": [{"id": "seg"}]} - call = mock_to_thread.await_args - assert call.args[0] is repo.requests.get - assert call.args[1] == "https://openpecha.example/v2/annotations/ann_2" - - -@pytest.mark.asyncio -async def test_get_segment_content_success_posts_segment_ids_payload(): - from pecha_api.text_uploader.segments import segment_respository as repo - - response = DummyResponse([{"segment_id": "s1", "content": "c1"}]) - with patch( - "pecha_api.text_uploader.segments.segment_respository.asyncio.to_thread", - new_callable=AsyncMock, - return_value=response, - ) as mock_to_thread: - result = await repo.get_segment_content( - segment_id=["s1", "s2"], - pecha_text_id="P1", - openpecha_api_url="https://openpecha.example", - ) - - assert result == [{"segment_id": "s1", "content": "c1"}] - call = mock_to_thread.await_args - assert call.args[0] is repo.requests.post - assert call.args[1] == "https://openpecha.example/v2/instances/P1/segment-content" - assert call.kwargs["json"] == {"segment_ids": ["s1", "s2"]} - - -@pytest.mark.asyncio -async def test_get_segment_content_raises_on_http_error(): - from pecha_api.text_uploader.segments import segment_respository as repo - - http_error = requests.exceptions.HTTPError("boom") - response = DummyResponse( - {"detail": "bad"}, - ok=False, - status_code=400, - text="bad", - raise_for_status_exc=http_error, - ) - with patch( - "pecha_api.text_uploader.segments.segment_respository.asyncio.to_thread", - new_callable=AsyncMock, - return_value=response, - ): - with pytest.raises(requests.exceptions.HTTPError): - await repo.get_segment_content( - segment_id=["s1"], - pecha_text_id="P1", - openpecha_api_url="https://openpecha.example", - ) - -@pytest.mark.asyncio -async def test_get_segment_content_raises_on_request_exception(): - from pecha_api.text_uploader.segments import segment_respository as repo - - with patch( - "pecha_api.text_uploader.segments.segment_respository.asyncio.to_thread", - new_callable=AsyncMock, - side_effect=requests.exceptions.RequestException("boom"), - ): - with pytest.raises(requests.exceptions.RequestException): - await repo.get_segment_content( - segment_id=["s1"], - pecha_text_id="P1", - openpecha_api_url="https://openpecha.example", - ) - - -@pytest.mark.asyncio -async def test_get_segment_content_raises_on_unexpected_exception(): - from pecha_api.text_uploader.segments import segment_respository as repo - - with patch( - "pecha_api.text_uploader.segments.segment_respository.asyncio.to_thread", - new_callable=AsyncMock, - side_effect=RuntimeError("boom"), - ): - with pytest.raises(RuntimeError): - await repo.get_segment_content( - segment_id=["s1"], - pecha_text_id="P1", - openpecha_api_url="https://openpecha.example", - ) - - -@pytest.mark.asyncio -async def test_post_segments_returns_json_when_ok(): - from pecha_api.text_uploader.segments import segment_respository as repo - - response = DummyResponse({"id": "seg_1"}, ok=True) - with patch( - "pecha_api.text_uploader.segments.segment_respository.asyncio.to_thread", - new_callable=AsyncMock, - return_value=response, - ) as mock_to_thread: - result = await repo.post_segments( - segments_payload={"text_id": "t1", "segments": []}, - destination_url="https://dest.example", - token="tok", - ) - - assert result == {"id": "seg_1"} - call = mock_to_thread.await_args - assert call.args[0] is repo.requests.post - assert call.args[1] == "https://dest.example/segments" - assert call.kwargs["headers"]["Authorization"] == "Bearer tok" - - -@pytest.mark.asyncio -async def test_post_segments_raises_for_non_ok_response(): - from pecha_api.text_uploader.segments import segment_respository as repo - - response = DummyResponse( - {"detail": "bad"}, - ok=False, - status_code=400, - text="bad", - raise_for_status_exc=requests.exceptions.HTTPError("boom"), - ) - with patch( - "pecha_api.text_uploader.segments.segment_respository.asyncio.to_thread", - new_callable=AsyncMock, - return_value=response, - ): - with pytest.raises(requests.exceptions.HTTPError): - await repo.post_segments( - segments_payload={"text_id": "t1", "segments": []}, - destination_url="https://dest.example", - token="tok", - ) - diff --git a/tests/text_uploader/segments/test_segment_service.py b/tests/text_uploader/segments/test_segment_service.py deleted file mode 100644 index 012f57ff7..000000000 --- a/tests/text_uploader/segments/test_segment_service.py +++ /dev/null @@ -1,221 +0,0 @@ -import pytest -from unittest.mock import AsyncMock, patch - -from pecha_api.text_uploader.segments.segment_service import SegmentService -from pecha_api.text_uploader.text_uploader_response_model import TextUploadRequest - - -def test_segment_service_parse_segments_content_slices_base_text(): - service = SegmentService() - base_text = "abcdef" - segments = [ - {"id": "s1", "span": {"start": 0, "end": 3}}, - {"id": "s2", "span": {"start": 3, "end": 6}}, - ] - - parsed = service.parse_segments_content(segments_annotation=segments, base_text=base_text) - assert parsed == [ - {"segment_id": "s1", "content": "abc"}, - {"segment_id": "s2", "content": "def"}, - ] - - -def test_segment_service_get_annotation_ids_filters_segmentation_only(): - service = SegmentService() - instance = { - "annotations": [ - {"type": "topic", "annotation_id": "x"}, - {"type": "segmentation", "annotation_id": "seg_1"}, - {"type": "segmentation", "annotation_id": "seg_2"}, - ] - } - - assert service.get_annotation_ids(instance) == ["seg_1", "seg_2"] - - -@pytest.mark.asyncio -async def test_segment_service_is_text_segments_uploaded_true_when_any_segments_exist(): - service = SegmentService() - with patch( - "pecha_api.text_uploader.segments.segment_service.get_segments_by_text_id", - new_callable=AsyncMock, - return_value=[{"id": "1"}], - ): - assert await service.is_text_segments_uploaded("t1") is True - - -@pytest.mark.asyncio -async def test_segment_service_upload_bulk_segments_batches_and_posts(): - service = SegmentService() - request = TextUploadRequest( - destination_url="https://dest.example/api/v1", - openpecha_api_url="https://openpecha.example", - text_id="T1", - ) - segments_content = [{"segment_id": f"s{i}", "content": f"c{i}"} for i in range(401)] - - with patch( - "pecha_api.text_uploader.segments.segment_service.post_segments", - new_callable=AsyncMock, - return_value={"ok": True}, - ) as mock_post: - await service.upload_bulk_segments( - text_id="wb_text_1", - segments_content=segments_content, - text_upload_request=request, - token="tok", - ) - - assert mock_post.await_count == 2 - first_payload = mock_post.await_args_list[0].args[0] - second_payload = mock_post.await_args_list[1].args[0] - assert first_payload["text_id"] == "wb_text_1" - assert len(first_payload["segments"]) == 400 - assert len(second_payload["segments"]) == 1 - assert first_payload["segments"][0]["type"] == "source" - - -@pytest.mark.asyncio -async def test_segment_service_get_segments_by_id_list_calls_repo(): - service = SegmentService() - request = TextUploadRequest( - destination_url="https://dest.example/api/v1", - openpecha_api_url="https://openpecha.example", - text_id="T1", - ) - - with patch( - "pecha_api.text_uploader.segments.segment_service.get_segments_by_id", - new_callable=AsyncMock, - return_value={"data": [{"id": "ann"}]}, - ) as mock_get: - result = await service.get_segments_by_id_list("ann_1", request) - - assert result == {"data": [{"id": "ann"}]} - mock_get.assert_awaited_once_with("ann_1", "https://openpecha.example") - - -@pytest.mark.asyncio -async def test_segment_service_upload_segments_skips_when_already_uploaded(): - service = SegmentService() - request = TextUploadRequest( - destination_url="https://dest.example/api/v1", - openpecha_api_url="https://openpecha.example", - text_id="T1", - ) - - with patch.object( - service, "is_text_segments_uploaded", new_callable=AsyncMock, return_value=True - ), patch( - "pecha_api.text_uploader.segments.segment_service.get_segments_annotation", - new_callable=AsyncMock, - ) as mock_get_annotation: - await service.upload_segments( - text_upload_request=request, - text_ids={"wb_text_1": "pecha_instance_1"}, - token="tok", - ) - - assert mock_get_annotation.called is False - -import pytest -from types import SimpleNamespace -from unittest.mock import AsyncMock, patch - -from pecha_api.text_uploader.segments.segment_service import SegmentService - - -def test_get_annotation_ids_filters_segmentation_only(): - service = SegmentService() - instance = { - "annotations": [ - {"type": "note", "annotation_id": "a0"}, - {"type": "segmentation", "annotation_id": "a1"}, - {"type": "segmentation", "annotation_id": "a2"}, - ] - } - - assert service.get_annotation_ids(instance) == ["a1", "a2"] - - -def test_get_annotation_ids_handles_none_annotations(): - service = SegmentService() - assert service.get_annotation_ids({"annotations": None}) == [] - - -def test_parse_segments_content_slices_base_text(): - service = SegmentService() - base_text = "abcdef" - segments_annotation = [ - {"id": "s1", "span": {"start": 0, "end": 2}}, - {"id": "s2", "span": {"start": 2, "end": 6}}, - ] - - assert service.parse_segments_content(segments_annotation, base_text) == [ - {"segment_id": "s1", "content": "ab"}, - {"segment_id": "s2", "content": "cdef"}, - ] - - -@pytest.mark.asyncio -async def test_is_text_segments_uploaded_true_when_any_segments_exist(): - service = SegmentService() - with patch( - "pecha_api.text_uploader.segments.segment_service.get_segments_by_text_id", - new_callable=AsyncMock, - return_value=[{"id": "seg_1"}], - ): - assert await service.is_text_segments_uploaded("text_1") is True - - -@pytest.mark.asyncio -async def test_upload_bulk_segments_batches_and_posts_payload(): - service = SegmentService() - text_upload_request = SimpleNamespace(destination_url="https://dest.example") - token = "t0k3n" - - # 401 segments -> two batches (400 + 1) - segments_content = [ - {"segment_id": f"s{i}", "content": f"c{i}"} for i in range(401) - ] - - with patch( - "pecha_api.text_uploader.segments.segment_service.post_segments", - new_callable=AsyncMock, - return_value={"ok": True}, - ) as mock_post_segments: - await service.upload_bulk_segments( - text_id="text_1", - segments_content=segments_content, - text_upload_request=text_upload_request, - token=token, - ) - - assert mock_post_segments.await_count == 2 - - first_payload = mock_post_segments.await_args_list[0].args[0] - second_payload = mock_post_segments.await_args_list[1].args[0] - - assert first_payload["text_id"] == "text_1" - assert len(first_payload["segments"]) == 400 - assert first_payload["segments"][0] == { - "pecha_segment_id": "s0", - "content": "c0", - "type": "source", - } - - assert second_payload["text_id"] == "text_1" - assert len(second_payload["segments"]) == 1 - assert second_payload["segments"][0] == { - "pecha_segment_id": "s400", - "content": "c400", - "type": "source", - } - - # Verify destination/token plumbing (payload, destination_url, token) - assert mock_post_segments.await_args_list[0].args[1:] == ( - "https://dest.example", - token, - ) - - diff --git a/tests/text_uploader/test_pipeline.py b/tests/text_uploader/test_pipeline.py deleted file mode 100644 index aa97b258a..000000000 --- a/tests/text_uploader/test_pipeline.py +++ /dev/null @@ -1,158 +0,0 @@ -import pytest -from fastapi import HTTPException -from unittest.mock import AsyncMock, patch - -from pecha_api.error_contants import ErrorConstants -from pecha_api.text_uploader.pipeline import pipeline -from pecha_api.text_uploader.text_metadata.text_metadata_model import TextInstanceIds -from pecha_api.text_uploader.text_uploader_response_model import TextUploadRequest - - -@pytest.mark.asyncio -async def test_pipeline_raises_403_when_not_admin(): - request = TextUploadRequest( - destination_url="LOCAL", - openpecha_api_url="DEVELOPMENT", - text_id="T1", - ) - - with patch("pecha_api.text_uploader.pipeline.verify_admin_access", return_value=False): - with pytest.raises(HTTPException) as exc: - await pipeline(text_upload_request=request, token="tok") - - assert exc.value.status_code == 403 - assert exc.value.detail == ErrorConstants.ADMIN_ERROR_MESSAGE - - -@pytest.mark.asyncio -async def test_pipeline_runs_segment_toc_and_returns_new_texts_when_any_new(): - request = TextUploadRequest( - destination_url="LOCAL", - openpecha_api_url="DEVELOPMENT", - text_id="T1", - ) - instance_ids = TextInstanceIds( - new_text={"wb_text_1": "pecha_instance_1"}, - all_text={"T1": "pecha_instance_1"}, - ) - - with patch("pecha_api.text_uploader.pipeline.verify_admin_access", return_value=True), patch( - "pecha_api.text_uploader.pipeline.CollectionService" - ) as MockCollectionService, patch( - "pecha_api.text_uploader.pipeline.TextMetadataService" - ) as MockTextMetadataService, patch( - "pecha_api.text_uploader.pipeline.SegmentService" - ) as MockSegmentService, patch( - "pecha_api.text_uploader.pipeline.TocService" - ) as MockTocService, patch( - "pecha_api.text_uploader.pipeline.MappingService" - ) as MockMappingService: - MockCollectionService.return_value.upload_collections = AsyncMock() - MockTextMetadataService.return_value.upload_text_metadata_service = AsyncMock( - return_value=instance_ids - ) - MockSegmentService.return_value.upload_segments = AsyncMock() - MockTocService.return_value.upload_toc = AsyncMock() - MockMappingService.return_value.trigger_mapping = AsyncMock() - - response = await pipeline(text_upload_request=request, token="tok") - - assert response.message == {"wb_text_1": "pecha_instance_1"} - - # Ensure services are wired with the resolved destination/openpecha URLs. - payload = MockCollectionService.return_value.upload_collections.await_args.kwargs[ - "text_upload_request" - ] - assert payload.destination_url.startswith("http") - assert payload.openpecha_api_url.startswith("https://") - assert payload.text_id == "T1" - - MockSegmentService.return_value.upload_segments.assert_awaited_once() - MockTocService.return_value.upload_toc.assert_awaited_once() - MockMappingService.return_value.trigger_mapping.assert_not_awaited() - - -@pytest.mark.asyncio -async def test_pipeline_skips_segment_and_toc_when_no_new_texts(): - request = TextUploadRequest( - destination_url="LOCAL", - openpecha_api_url="DEVELOPMENT", - text_id="T1", - ) - instance_ids = TextInstanceIds(new_text={}, all_text={"T1": "pecha_instance_1"}) - - with patch("pecha_api.text_uploader.pipeline.verify_admin_access", return_value=True), patch( - "pecha_api.text_uploader.pipeline.CollectionService" - ) as MockCollectionService, patch( - "pecha_api.text_uploader.pipeline.TextMetadataService" - ) as MockTextMetadataService, patch( - "pecha_api.text_uploader.pipeline.SegmentService" - ) as MockSegmentService, patch( - "pecha_api.text_uploader.pipeline.TocService" - ) as MockTocService, patch( - "pecha_api.text_uploader.pipeline.MappingService" - ) as MockMappingService: - MockCollectionService.return_value.upload_collections = AsyncMock() - MockTextMetadataService.return_value.upload_text_metadata_service = AsyncMock( - return_value=instance_ids - ) - MockMappingService.return_value.trigger_mapping = AsyncMock() - - response = await pipeline(text_upload_request=request, token="tok") - - assert response.message == "All texts are already uploaded" - assert MockSegmentService.called is False - assert MockTocService.called is False - MockMappingService.return_value.trigger_mapping.assert_not_awaited() - - payload = MockCollectionService.return_value.upload_collections.await_args.kwargs[ - "text_upload_request" - ] - # Mapping is intentionally skipped when destination_url is LOCAL. - - -@pytest.mark.asyncio -async def test_pipeline_triggers_mapping_when_destination_is_not_local(): - request = TextUploadRequest( - destination_url="DEVELOPMENT", - openpecha_api_url="DEVELOPMENT", - text_id="T1", - ) - instance_ids = TextInstanceIds( - new_text={"wb_text_1": "pecha_instance_1"}, - all_text={"T1": "pecha_instance_1"}, - ) - - with patch("pecha_api.text_uploader.pipeline.verify_admin_access", return_value=True), patch( - "pecha_api.text_uploader.pipeline.CollectionService" - ) as MockCollectionService, patch( - "pecha_api.text_uploader.pipeline.TextMetadataService" - ) as MockTextMetadataService, patch( - "pecha_api.text_uploader.pipeline.SegmentService" - ) as MockSegmentService, patch( - "pecha_api.text_uploader.pipeline.TocService" - ) as MockTocService, patch( - "pecha_api.text_uploader.pipeline.MappingService" - ) as MockMappingService: - MockCollectionService.return_value.upload_collections = AsyncMock() - MockTextMetadataService.return_value.upload_text_metadata_service = AsyncMock( - return_value=instance_ids - ) - MockSegmentService.return_value.upload_segments = AsyncMock() - MockTocService.return_value.upload_toc = AsyncMock() - MockMappingService.return_value.trigger_mapping = AsyncMock() - - response = await pipeline(text_upload_request=request, token="tok") - - assert response.message == {"wb_text_1": "pecha_instance_1"} - MockMappingService.return_value.trigger_mapping.assert_awaited_once() - - trigger_kwargs = MockMappingService.return_value.trigger_mapping.await_args.kwargs - assert trigger_kwargs["text_ids"] == {"T1": "pecha_instance_1"} - - called_payload = trigger_kwargs["text_upload_request"] - # Mapping is triggered with the original (enum) request values, not the resolved URL payload. - assert called_payload.destination_url == "DEVELOPMENT" - assert called_payload.openpecha_api_url == "DEVELOPMENT" - assert called_payload.text_id == "T1" - diff --git a/tests/text_uploader/test_text_uploader_views.py b/tests/text_uploader/test_text_uploader_views.py deleted file mode 100644 index a3b4b2042..000000000 --- a/tests/text_uploader/test_text_uploader_views.py +++ /dev/null @@ -1,181 +0,0 @@ -from fastapi.testclient import TestClient -from unittest.mock import patch, AsyncMock -import pytest - -from pecha_api.app import api -from pecha_api.error_contants import ErrorConstants -from pecha_api.text_uploader.text_uploader_response_model import TextUploadResponse - -client = TestClient(api) - - -class TestTextUploaderViews: - """Test cases for text uploader views.""" - - @pytest.fixture - def mock_text_upload_request(self): - """Fixture for text upload request data.""" - return { - "destination_url": "DEVELOPMENT", - "openpecha_api_url": "DEVELOPMENT", - "text_id": "test_text_123" - } - - @pytest.fixture - def admin_token(self): - """Fixture for admin authentication token.""" - return "Bearer admin_test_token" - - @pytest.fixture - def non_admin_token(self): - """Fixture for non-admin authentication token.""" - return "Bearer non_admin_test_token" - - def test_upload_text_success_with_admin_token(self, mock_text_upload_request, admin_token): - """Test successful text upload with admin token.""" - with patch("pecha_api.text_uploader.text_uploader_views.pipeline", new_callable=AsyncMock) as mock_pipeline: - mock_pipeline.return_value = TextUploadResponse(message={"text_1": "instance_1"}) - - response = client.post( - "/text-uploader", - json=mock_text_upload_request, - headers={"Authorization": admin_token} - ) - - assert response.status_code == 200 - assert response.json() == {"message": {"text_1": "instance_1"}} - mock_pipeline.assert_called_once() - - def test_upload_text_forbidden_with_non_admin_token(self, mock_text_upload_request, non_admin_token): - """Test text upload fails with non-admin token (403 Forbidden).""" - with patch("pecha_api.text_uploader.pipeline.verify_admin_access") as mock_verify_admin: - mock_verify_admin.return_value = False - - response = client.post( - "/text-uploader", - json=mock_text_upload_request, - headers={"Authorization": non_admin_token} - ) - - assert response.status_code == 403 - assert response.json()["detail"] == ErrorConstants.ADMIN_ERROR_MESSAGE - - def test_upload_text_unauthorized_without_token(self, mock_text_upload_request): - """Test text upload fails without authentication token (401 Unauthorized).""" - response = client.post( - "/text-uploader", - json=mock_text_upload_request - ) - - assert response.status_code == 403 - - def test_upload_text_invalid_request_missing_destination_url(self, admin_token): - """Test text upload fails with missing destination_url.""" - invalid_request = { - "openpecha_api_url": "DEVELOPMENT", - "text_id": "test_text_123" - } - - response = client.post( - "/text-uploader", - json=invalid_request, - headers={"Authorization": admin_token} - ) - - assert response.status_code == 422 - - def test_upload_text_invalid_request_missing_openpecha_api_url(self, admin_token): - """Test text upload fails with missing openpecha_api_url.""" - invalid_request = { - "destination_url": "DEVELOPMENT", - "text_id": "test_text_123" - } - - response = client.post( - "/text-uploader", - json=invalid_request, - headers={"Authorization": admin_token} - ) - - assert response.status_code == 422 - - def test_upload_text_invalid_request_missing_text_id(self, admin_token): - """Test text upload fails with missing text_id.""" - invalid_request = { - "destination_url": "DEVELOPMENT", - "openpecha_api_url": "DEVELOPMENT" - } - - response = client.post( - "/text-uploader", - json=invalid_request, - headers={"Authorization": admin_token} - ) - - assert response.status_code == 422 - - def test_upload_text_empty_request_body(self, admin_token): - """Test text upload fails with empty request body.""" - response = client.post( - "/text-uploader", - json={}, - headers={"Authorization": admin_token} - ) - - assert response.status_code == 422 - - def test_upload_text_pipeline_exception_handling(self, mock_text_upload_request, admin_token): - """Test that exceptions from pipeline are properly propagated.""" - with patch("pecha_api.text_uploader.text_uploader_views.pipeline", new_callable=AsyncMock) as mock_pipeline: - mock_pipeline.side_effect = Exception("Pipeline processing failed") - - with pytest.raises(Exception) as exc_info: - response = client.post( - "/text-uploader", - json=mock_text_upload_request, - headers={"Authorization": admin_token} - ) - - assert str(exc_info.value) == "Pipeline processing failed" - - def test_upload_text_with_special_characters_in_text_id(self, admin_token): - """Test text upload with special characters in text_id.""" - request_with_special_chars = { - "destination_url": "DEVELOPMENT", - "openpecha_api_url": "DEVELOPMENT", - "text_id": "test_text_!@#$%^&*()" - } - - with patch("pecha_api.text_uploader.text_uploader_views.pipeline", new_callable=AsyncMock) as mock_pipeline: - mock_pipeline.return_value = TextUploadResponse(message={"text_special": "instance_special"}) - - response = client.post( - "/text-uploader", - json=request_with_special_chars, - headers={"Authorization": admin_token} - ) - - assert response.status_code == 200 - assert response.json() == {"message": {"text_special": "instance_special"}} - - def test_upload_text_with_extra_fields(self, admin_token): - """Test text upload with extra fields in request (should be ignored by Pydantic).""" - request_with_extra_fields = { - "destination_url": "DEVELOPMENT", - "openpecha_api_url": "DEVELOPMENT", - "text_id": "test_text_123", - "extra_field": "should_be_ignored" - } - - with patch("pecha_api.text_uploader.text_uploader_views.pipeline", new_callable=AsyncMock) as mock_pipeline: - mock_pipeline.return_value = TextUploadResponse(message={"text_extra": "instance_extra"}) - - response = client.post( - "/text-uploader", - json=request_with_extra_fields, - headers={"Authorization": admin_token} - ) - - assert response.status_code == 200 - assert response.json() == {"message": {"text_extra": "instance_extra"}} - diff --git a/tests/text_uploader/texts/__init__.py b/tests/text_uploader/texts/__init__.py deleted file mode 100644 index cc5244580..000000000 --- a/tests/text_uploader/texts/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Text-related tests for text uploader.""" - - - - - diff --git a/tests/text_uploader/texts/test_text_metadata_service.py b/tests/text_uploader/texts/test_text_metadata_service.py deleted file mode 100644 index 7781ed6d5..000000000 --- a/tests/text_uploader/texts/test_text_metadata_service.py +++ /dev/null @@ -1,573 +0,0 @@ -import pytest -from unittest.mock import AsyncMock, patch, MagicMock - -from pecha_api.text_uploader.text_metadata.text_metadata_service import TextMetadataService -from pecha_api.text_uploader.text_metadata.text_metadata_model import ( - CriticalInstance, - CriticalInstanceResponse, - TextInstanceIds -) -from pecha_api.text_uploader.text_uploader_response_model import TextUploadRequest - - -@pytest.mark.asyncio -async def test_create_textmetada_payload_version_sets_group_and_category_from_collection(): - service = TextMetadataService() - service.version_group_id = "group_v1" - - text_metadata = { - "language": "en", - "title": {"en": "A Title"}, - "category_id": "pecha_cat_1", - "isPublished": True, - "views": 12, - "ranking": 3, - "license": "CC0", - } - - text_upload_request = TextUploadRequest( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - text_id="t1" - ) - - with patch.object( - service, - "get_text_critical_instance", - new_callable=AsyncMock, - return_value=CriticalInstanceResponse( - critical_instances=[ - CriticalInstance( - id="inst_1", - type="critical", - source="https://source.example", - alt_incipit_titles=None, - ) - ] - ), - ), patch.object( - service, - "get_wb_collection_id", - new_callable=AsyncMock, - return_value="wb_cat_1" - ): - payload = await service.create_textmetada_payload( - text_id="t1", - text_metadata=text_metadata, - type="version", - text_upload_request=text_upload_request - ) - - assert payload.pecha_text_id == "inst_1" - assert payload.title == "A Title" - assert payload.language == "en" - assert payload.group_id == "group_v1" - assert payload.categories == ["wb_cat_1"] - assert payload.type == "version" - assert payload.source_link == "https://source.example" - - -@pytest.mark.asyncio -async def test_create_textmetada_payload_commentary_sets_group_and_category_from_version_group(): - service = TextMetadataService() - service.version_group_id = "group_v1" - service.commentary_group_id = "group_c1" - - text_metadata = { - "language": "bo", - "title": {"bo": "མཚན་བྱང་"}, - } - - text_upload_request = TextUploadRequest( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - text_id="t_commentary" - ) - - with patch.object( - service, - "get_text_critical_instance", - new_callable=AsyncMock, - return_value=CriticalInstanceResponse( - critical_instances=[ - CriticalInstance( - id="inst_c1", - type="critical", - source="s", - alt_incipit_titles=None, - ) - ] - ), - ): - payload = await service.create_textmetada_payload( - text_id="t_commentary", - text_metadata=text_metadata, - type="commentary", - text_upload_request=text_upload_request - ) - - assert payload.group_id == "group_c1" - assert payload.categories == ["group_v1"] - assert payload.type == "commentary" - - -@pytest.mark.asyncio -async def test_get_uploaded_texts_returns_uploaded_text_ids_and_instances_mapping(): - service = TextMetadataService() - - async def _get_critical_instance_side_effect(text_id: str, openpecha_api_url: str): - instance_id = {"t1": "inst_1", "t2": "inst_2"}[text_id] - return CriticalInstanceResponse( - critical_instances=[ - CriticalInstance( - id=instance_id, - type="critical", - source="src", - alt_incipit_titles=None, - ) - ] - ) - - # Only inst_2 is already uploaded in the destination DB - uploaded_text_row = {"pecha_text_id": "inst_2", "group_id": "g"} - - with patch.object( - service, - "get_text_critical_instance", - new_callable=AsyncMock, - side_effect=_get_critical_instance_side_effect, - ), patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.get_texts_by_pecha_text_ids", - new_callable=AsyncMock, - return_value=[uploaded_text_row], - ): - texts, uploaded_text_ids, instances = await service.get_uploaded_texts( - text_ids=["t1", "t2"], - openpecha_api_url="https://openpecha.example", - destination_url="https://destination.example" - ) - - assert texts == [uploaded_text_row] - assert uploaded_text_ids == ["t2"] - assert instances == {"t1": "inst_1", "t2": "inst_2"} - - -@pytest.mark.asyncio -async def test_get_wb_collection_id_raises_when_collection_missing(): - service = TextMetadataService() - with patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.get_collection_by_pecha_collection_id", - new_callable=AsyncMock, - return_value=None, - ): - with pytest.raises( - ValueError, match="Collection with pecha_collection_id pecha_missing not found" - ): - await service.get_wb_collection_id( - pecha_collection_id="pecha_missing", - destination_url="https://destination.example" - ) - - -@pytest.mark.asyncio -async def test_get_wb_collection_id_returns_collection_id(): - """Test get_wb_collection_id returns the collection ID when found""" - service = TextMetadataService() - - with patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.get_collection_by_pecha_collection_id", - new_callable=AsyncMock, - return_value="wb_collection_123" - ): - result = await service.get_wb_collection_id( - pecha_collection_id="pecha_collection_456", - destination_url="https://destination.example" - ) - - assert result == "wb_collection_123" - - -@pytest.mark.asyncio -async def test_upload_text_metadata_service_success(): - """Test upload_text_metadata_service with translation texts""" - service = TextMetadataService() - - text_upload_request = TextUploadRequest( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - text_id="text_1" - ) - - text_metadata = { - "type": "translation", - "language": "en", - "title": {"en": "Test Text"} - } - - text_related_by_work = { - "work_1": { - "relation": "translation", - "expression_ids": ["text_2", "text_3"] - } - } - - with patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.get_text_metadata", - new_callable=AsyncMock, - return_value=text_metadata - ), \ - patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.get_text_related_by_work", - new_callable=AsyncMock, - return_value=text_related_by_work - ), \ - patch.object( - service, - "get_text_meta_data_service", - new_callable=AsyncMock, - return_value={"new_text_id_1": "inst_1", "new_text_id_2": "inst_2"} - ) as mock_get_texts: - result = await service.upload_text_metadata_service( - text_upload_request=text_upload_request, - token="test_token" - ) - - assert isinstance(result, TextInstanceIds) - assert len(result.new_text) > 0 - # Verify the service was called for both translations and commentaries - assert mock_get_texts.call_count == 2 - # all_text reflects only what the real method accumulated; - # since get_text_meta_data_service is fully mocked here, all_instance_ids stays {} - assert result.all_text == {} - - -@pytest.mark.asyncio -async def test_upload_text_metadata_service_with_commentary(): - """Test upload_text_metadata_service with commentary texts""" - service = TextMetadataService() - - text_upload_request = TextUploadRequest( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - text_id="commentary_1" - ) - - text_metadata = { - "type": "commentary", - "language": "bo", - "title": {"bo": "མཚན་བྱང་"} - } - - text_related_by_work = { - "work_1": { - "relation": "sibling_commentary", - "expression_ids": ["commentary_2"] - } - } - - with patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.get_text_metadata", - new_callable=AsyncMock, - return_value=text_metadata - ), \ - patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.get_text_related_by_work", - new_callable=AsyncMock, - return_value=text_related_by_work - ), \ - patch.object( - service, - "get_text_meta_data_service", - new_callable=AsyncMock, - return_value={"commentary_id_1": "inst_c1"} - ): - result = await service.upload_text_metadata_service( - text_upload_request=text_upload_request, - token="test_token" - ) - - assert isinstance(result, TextInstanceIds) - assert result.new_text is not None - - -@pytest.mark.asyncio -async def test_get_text_meta_data_service_creates_group_for_first_text(): - """Test get_text_meta_data_service creates a new group for translations""" - service = TextMetadataService() - - text_upload_request = TextUploadRequest( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - text_id="text_1" - ) - - text_metadata = { - "language": "en", - "title": {"en": "Test Text"}, - "category_id": "cat_1" - } - - critical_instance = CriticalInstanceResponse( - critical_instances=[ - CriticalInstance( - id="inst_1", - type="critical", - source="https://source.example", - alt_incipit_titles=None, - ) - ] - ) - - with patch.object( - service, - "get_uploaded_texts", - new_callable=AsyncMock, - return_value=([], [], {"text_1": "inst_1"}) - ), \ - patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.get_text_metadata", - new_callable=AsyncMock, - return_value=text_metadata - ), \ - patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.post_group", - new_callable=AsyncMock, - return_value={"id": "new_group_id"} - ) as mock_post_group, \ - patch.object( - service, - "create_textmetada_payload", - new_callable=AsyncMock, - return_value=MagicMock() - ), \ - patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.post_text", - new_callable=AsyncMock, - return_value={"id": "new_text_id", "title": "Test Text"} - ): - result = await service.get_text_meta_data_service( - text_ids=["text_1"], - type="translation", - text_upload_request=text_upload_request, - token="test_token" - ) - - mock_post_group.assert_awaited_once() - assert service.version_group_id == "new_group_id" - assert "new_text_id" in result - assert service.all_instance_ids == {"text_1": "inst_1"} - - -@pytest.mark.asyncio -async def test_get_text_meta_data_service_skips_uploaded_texts(): - """Test get_text_meta_data_service skips already uploaded texts""" - service = TextMetadataService() - - text_upload_request = TextUploadRequest( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - text_id="text_1" - ) - - text_metadata = { - "language": "en", - "title": {"en": "Already Uploaded Text"}, - "category_id": "cat_1" - } - - # Mock that text_1 is already uploaded - with patch.object( - service, - "get_uploaded_texts", - new_callable=AsyncMock, - return_value=( - [{"id": "existing_id", "pecha_text_id": "inst_1", "group_id": "existing_group_id"}], - ["text_1"], # text_1 is already uploaded - {"text_1": "inst_1"} - ) - ), \ - patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.get_text_metadata", - new_callable=AsyncMock, - return_value=text_metadata - ), \ - patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.post_text", - new_callable=AsyncMock - ) as mock_post_text: - result = await service.get_text_meta_data_service( - text_ids=["text_1"], - type="translation", - text_upload_request=text_upload_request, - token="test_token" - ) - - # Should not post text since it's already uploaded - mock_post_text.assert_not_awaited() - assert result == {} - # Version group should be set from uploaded text - assert service.version_group_id == "existing_group_id" - assert service.all_instance_ids == {"text_1": "inst_1"} - - -@pytest.mark.asyncio -async def test_get_text_critical_instance_returns_critical_instances(): - """Test get_text_critical_instance returns CriticalInstanceResponse""" - service = TextMetadataService() - - expected_instances = [ - CriticalInstance( - id="inst_1", - type="critical", - source="https://source1.example", - alt_incipit_titles=None, - ), - CriticalInstance( - id="inst_2", - type="critical", - source="https://source2.example", - alt_incipit_titles=None, - ) - ] - - with patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.get_critical_instances", - new_callable=AsyncMock, - return_value=CriticalInstanceResponse(critical_instances=expected_instances) - ): - result = await service.get_text_critical_instance( - text_id="text_1", - openpecha_api_url="https://openpecha.example" - ) - - assert isinstance(result, CriticalInstanceResponse) - assert len(result.critical_instances) == 2 - assert result.critical_instances[0].id == "inst_1" - - -@pytest.mark.asyncio -async def test_get_text_meta_data_service_commentary_creates_separate_group(): - """Test get_text_meta_data_service creates separate group for commentary""" - service = TextMetadataService() - service.version_group_id = "version_group_123" - - text_upload_request = TextUploadRequest( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - text_id="commentary_1" - ) - - text_metadata = { - "language": "bo", - "title": {"bo": "མཚན་བྱང་"}, - "category_id": "cat_1" - } - - with patch.object( - service, - "get_uploaded_texts", - new_callable=AsyncMock, - return_value=([], [], {"commentary_1": "inst_c1"}) - ), \ - patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.get_text_metadata", - new_callable=AsyncMock, - return_value=text_metadata - ), \ - patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.post_group", - new_callable=AsyncMock, - return_value={"id": "commentary_group_id"} - ) as mock_post_group, \ - patch.object( - service, - "create_textmetada_payload", - new_callable=AsyncMock, - return_value=MagicMock() - ), \ - patch( - "pecha_api.text_uploader.text_metadata.text_metadata_service.post_text", - new_callable=AsyncMock, - return_value={"id": "commentary_text_id", "title": "Commentary"} - ): - result = await service.get_text_meta_data_service( - text_ids=["commentary_1"], - type="commentary", - text_upload_request=text_upload_request, - token="test_token" - ) - - # Should create commentary group - mock_post_group.assert_awaited_once_with('commentary', text_upload_request.destination_url, "test_token") - assert service.commentary_group_id == "commentary_group_id" - assert "commentary_text_id" in result - assert service.all_instance_ids == {"commentary_1": "inst_c1"} - - -@pytest.mark.asyncio -async def test_all_instance_ids_merges_translation_and_commentary_instances(): - """all_instance_ids must accumulate across both translation and commentary calls. - - The old bug used `=` (overwrite) instead of `.update()`, so only the last - batch's instances survived. With the fix, both batches are merged. - """ - service = TextMetadataService() - service.version_group_id = "version_group_123" - - text_upload_request = TextUploadRequest( - destination_url="https://destination.example", - openpecha_api_url="https://openpecha.example", - text_id="text_1" - ) - - translation_instances = {"text_1": "inst_t1", "text_2": "inst_t2"} - commentary_instances = {"commentary_1": "inst_c1"} - - translation_metadata = {"language": "en", "title": {"en": "T"}, "category_id": "cat_1"} - commentary_metadata = {"language": "bo", "title": {"bo": "C"}, "category_id": "cat_1"} - - # ── First call: translations ────────────────────────────────────────────── - with patch.object(service, "get_uploaded_texts", new_callable=AsyncMock, - return_value=([], [], translation_instances)), \ - patch("pecha_api.text_uploader.text_metadata.text_metadata_service.get_text_metadata", - new_callable=AsyncMock, return_value=translation_metadata), \ - patch("pecha_api.text_uploader.text_metadata.text_metadata_service.post_group", - new_callable=AsyncMock, return_value={"id": "grp_t"}), \ - patch.object(service, "create_textmetada_payload", - new_callable=AsyncMock, return_value=MagicMock()), \ - patch("pecha_api.text_uploader.text_metadata.text_metadata_service.post_text", - new_callable=AsyncMock, return_value={"id": "t_id", "title": "T"}): - await service.get_text_meta_data_service( - text_ids=["text_1", "text_2"], - type="translation", - text_upload_request=text_upload_request, - token="test_token" - ) - - assert service.all_instance_ids == translation_instances - - # ── Second call: commentaries ───────────────────────────────────────────── - with patch.object(service, "get_uploaded_texts", new_callable=AsyncMock, - return_value=([], [], commentary_instances)), \ - patch("pecha_api.text_uploader.text_metadata.text_metadata_service.get_text_metadata", - new_callable=AsyncMock, return_value=commentary_metadata), \ - patch("pecha_api.text_uploader.text_metadata.text_metadata_service.post_group", - new_callable=AsyncMock, return_value={"id": "grp_c"}), \ - patch.object(service, "create_textmetada_payload", - new_callable=AsyncMock, return_value=MagicMock()), \ - patch("pecha_api.text_uploader.text_metadata.text_metadata_service.post_text", - new_callable=AsyncMock, return_value={"id": "c_id", "title": "C"}): - await service.get_text_meta_data_service( - text_ids=["commentary_1"], - type="commentary", - text_upload_request=text_upload_request, - token="test_token" - ) - - # Both translation AND commentary instances must be present. - # The old `self.all_instance_ids = instances` (overwrite) would have left - # only {"commentary_1": "inst_c1"} here, missing the 2 translation entries. - expected = {**translation_instances, **commentary_instances} - assert service.all_instance_ids == expected - - diff --git a/tests/text_uploader/texts/test_toc_service.py b/tests/text_uploader/texts/test_toc_service.py deleted file mode 100644 index b48a3ff23..000000000 --- a/tests/text_uploader/texts/test_toc_service.py +++ /dev/null @@ -1,105 +0,0 @@ -import pytest -from types import SimpleNamespace -from unittest.mock import AsyncMock, patch - -from pecha_api.text_uploader.table_of_content.toc_service import TocService - - -@pytest.mark.asyncio -async def test_order_segments_by_annotation_span_sorts_and_numbers(): - service = TocService() - annotation_segments = { - "data": [ - {"id": "s2", "span": {"start": 5, "end": 7}}, - {"id": "s1", "span": {"start": 0, "end": 2}}, - {"id": "s3", "span": {"start": 8, "end": 9}}, - ] - } - - result = await service.order_segments_by_annotation_span(annotation_segments) - - assert result == [ - {"segment_id": "s1", "segment_number": 1}, - {"segment_id": "s2", "segment_number": 2}, - {"segment_id": "s3", "segment_number": 3}, - ] - - -def test_create_toc_payload_uses_uuid_and_wraps_segments(): - service = TocService() - ordered_segments = [ - {"segment_id": "s1", "segment_number": 1}, - {"segment_id": "s2", "segment_number": 2}, - ] - - with patch( - "pecha_api.text_uploader.table_of_content.toc_service.uuid.uuid4", - return_value="fixed-uuid", - ): - payload = service.create_toc_payload(ordered_segments, text_id="t1") - - assert payload == { - "text_id": "t1", - "type": "text", - "sections": [ - { - "id": "fixed-uuid", - "title": "1", - "section_number": 1, - "segments": ordered_segments, - } - ], - } - - -@pytest.mark.asyncio -async def test_upload_toc_builds_payload_and_posts(): - service = TocService() - text_upload_request = SimpleNamespace( - destination_url="https://dest.example", - openpecha_api_url="https://openpecha.example", - text_id="ignored", - ) - - # Mocks for SegmentService instance methods used by TocService - service.segment_service.get_segments_annotation_by_pecha_text_id = AsyncMock( - return_value={"annotations": [{"type": "segmentation", "annotation_id": "ann_1"}]} - ) - # SegmentService.get_annotation_ids is synchronous. - service.segment_service.get_annotation_ids = lambda _instance: ["ann_1"] - service.segment_service.get_segments_by_id_list = AsyncMock( - return_value={ - "data": [ - {"id": "s2", "span": {"start": 2, "end": 3}}, - {"id": "s1", "span": {"start": 0, "end": 1}}, - ] - } - ) - - with patch( - "pecha_api.text_uploader.table_of_content.toc_service.uuid.uuid4", - return_value="fixed-uuid", - ), patch( - "pecha_api.text_uploader.table_of_content.toc_service.post_toc", - new_callable=AsyncMock, - return_value={"ok": True}, - ) as mock_post_toc: - await service.upload_toc( - text_ids={"local_text_1": "pecha_text_1"}, - text_upload_request=text_upload_request, - token="tok", - ) - - mock_post_toc.assert_awaited_once() - posted_payload, posted_destination, posted_token = mock_post_toc.await_args.args - - assert posted_destination == "https://dest.example" - assert posted_token == "tok" - assert posted_payload["text_id"] == "local_text_1" - assert posted_payload["sections"][0]["id"] == "fixed-uuid" - assert posted_payload["sections"][0]["segments"] == [ - {"segment_id": "s1", "segment_number": 1}, - {"segment_id": "s2", "segment_number": 2}, - ] - - diff --git a/tests/texts/mappings/__init__.py b/tests/texts/mappings/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/texts/mappings/test_delete_mappings.py b/tests/texts/mappings/test_delete_mappings.py deleted file mode 100644 index 3f9f3d5c4..000000000 --- a/tests/texts/mappings/test_delete_mappings.py +++ /dev/null @@ -1,157 +0,0 @@ -import json -import pytest -from unittest.mock import patch, MagicMock - -from fastapi.testclient import TestClient -from fastapi import HTTPException, status -from fastapi.routing import APIRoute - -from pecha_api.app import api -from pecha_api.texts.segments.segments_response_models import SegmentResponse, MappingResponse, SegmentDTO -from pecha_api.error_contants import ErrorConstants -from pecha_api.texts.mappings.mappings_response_models import TextMappingRequest - -client = TestClient(api) - -# Test data -text_mapping_delete_request = { - "text_mappings": [ - { - "text_id": "2ff4215e-bc9e-4d16-8d7e-b4adea3c6ef9", - "segment_id": "cce14575-ebc3-43aa-bcce-777676f3b2e2", - "mappings": [ - { - "parent_text_id": "e55d66bc-0b2c-4575-afe1-c357856b1592", - "segments": [ - "5bbe24b9-625e-41bf-b6aa-a949f26a7c05", - "83311e49-7e8b-413d-95c3-80d2cdea5158" - ] - } - ] - } - ] -} - -@pytest.mark.asyncio -@patch("pecha_api.texts.mappings.mappings_views.delete_segment_mapping") -async def test_delete_text_mapping_success(mock_delete_mapping): - # Arrange - test_token = "test_token" - # The delete_segment_mapping function returns None for successful deletion - mock_delete_mapping.return_value = None - - # We need to directly test the endpoint handler function rather than using the TestClient - # for DELETE requests with a body - from pecha_api.texts.mappings.mappings_views import delete_text_mapping - - # Create a mock request object - request = TextMappingRequest(**text_mapping_delete_request) - auth_credentials = MagicMock() - auth_credentials.credentials = test_token - - # Call the endpoint handler directly - result = await delete_text_mapping(auth_credentials, request) - - # Assert - assert result is None - mock_delete_mapping.assert_called_once_with(token=test_token, text_mapping_request=request) - -def test_delete_text_mapping_unauthorized(): - # For unauthorized test, we can use the TestClient without a body - # since we're just testing the auth middleware - response = client.delete( - "/mappings" - ) - - # Assert - assert response.status_code == 403 - -@pytest.mark.asyncio -@patch("pecha_api.texts.mappings.mappings_views.delete_segment_mapping") -async def test_delete_text_mapping_forbidden(mock_delete_mapping): - # Arrange - mock_delete_mapping.side_effect = HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=ErrorConstants.ADMIN_ERROR_MESSAGE - ) - - # Test directly with the handler function - from pecha_api.texts.mappings.mappings_views import delete_text_mapping - - # Create a mock request object - request = TextMappingRequest(**text_mapping_delete_request) - auth_credentials = MagicMock() - auth_credentials.credentials = "token" - - # Act & Assert - with pytest.raises(HTTPException) as exc_info: - await delete_text_mapping(auth_credentials, request) - - # Verify the exception details - assert exc_info.value.status_code == 403 - assert exc_info.value.detail == ErrorConstants.ADMIN_ERROR_MESSAGE - -@pytest.mark.asyncio -@patch("pecha_api.texts.mappings.mappings_views.delete_segment_mapping") -async def test_delete_text_mapping_not_found(mock_delete_mapping): - # Arrange - mock_delete_mapping.side_effect = HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Segment not found" - ) - - # Test directly with the handler function - from pecha_api.texts.mappings.mappings_views import delete_text_mapping - - # Create a mock request object - request = TextMappingRequest(**text_mapping_delete_request) - auth_credentials = MagicMock() - auth_credentials.credentials = "token" - - # Act & Assert - with pytest.raises(HTTPException) as exc_info: - await delete_text_mapping(auth_credentials, request) - - # Verify the exception details - assert exc_info.value.status_code == 404 - assert exc_info.value.detail == "Segment not found" - -@pytest.mark.asyncio -@patch("pecha_api.texts.mappings.mappings_views.delete_segment_mapping") -async def test_delete_text_mapping_bad_request(mock_delete_mapping): - # Arrange - mock_delete_mapping.side_effect = HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ErrorConstants.SEGMENT_MAPPING_ERROR_MESSAGE - ) - - # Test directly with the handler function - from pecha_api.texts.mappings.mappings_views import delete_text_mapping - - # Create a mock request object - request = TextMappingRequest(**text_mapping_delete_request) - auth_credentials = MagicMock() - auth_credentials.credentials = "token" - - # Act & Assert - with pytest.raises(HTTPException) as exc_info: - await delete_text_mapping(auth_credentials, request) - - # Verify the exception details - assert exc_info.value.status_code == 400 - assert exc_info.value.detail == ErrorConstants.SEGMENT_MAPPING_ERROR_MESSAGE - -def test_delete_text_mapping_invalid_request(): - # For validation errors, we can use a POST request to test the validation - # since the validation happens before the request reaches the handler - response = client.post( - "/mappings", - headers={ - "Authorization": "Bearer token", - "Content-Type": "application/json" - }, - json={"invalid": "request"} - ) - - # Assert - validation errors return 422 - assert response.status_code == 422 diff --git a/tests/texts/mappings/test_mapping_updates.py b/tests/texts/mappings/test_mapping_updates.py deleted file mode 100644 index 81338460d..000000000 --- a/tests/texts/mappings/test_mapping_updates.py +++ /dev/null @@ -1,164 +0,0 @@ -import uuid -import pytest -from typing import Dict, List -from unittest.mock import AsyncMock, Mock, patch - -from pecha_api.texts.mappings.mappings_service import ( - _merge_segment_mappings, - _get_existing_mappings, - _process_new_mappings, - _construct_update_segments -) -from pecha_api.texts.segments.segments_models import Mapping, Segment - -@pytest.fixture -def mock_segment(): - """Create a mock segment that doesn't require database initialization""" - def _create_segment(id_str: str, text_id: str, content: str, mapping: List[Mapping] = None): - segment = Mock(spec=Segment) - segment.id = uuid.UUID(id_str) if isinstance(id_str, str) else id_str - segment.text_id = text_id - segment.content = content - segment.mapping = mapping or [] - return segment - return _create_segment - - -def test_merge_segment_mappings(): - """Test merging of two segment mappings""" - # Arrange - existing_mapping = Mapping(text_id="text1", segments=["seg1", "seg2"]) - new_mapping = Mapping(text_id="text1", segments=["seg2", "seg3"]) - - # Act - result = _merge_segment_mappings(existing_mapping, new_mapping) - - # Assert - assert result.text_id == "text1" - assert set(result.segments) == {"seg1", "seg2", "seg3"} - assert result is existing_mapping # Should modify existing mapping - - -def test_get_existing_mappings(mock_segment): - """Test getting dictionary of existing mappings""" - # Arrange - mappings = [ - Mapping(text_id="text1", segments=["seg1"]), - Mapping(text_id="text2", segments=["seg2"]) - ] - segment = mock_segment( - id_str="12345678-1234-5678-1234-567812345678", - text_id="source", - content="content", - mapping=mappings - ) - - # Act - result = _get_existing_mappings(segment) - - # Assert - assert isinstance(result, dict) - assert len(result) == 2 - assert "text1" in result - assert "text2" in result - assert result["text1"].segments == ["seg1"] - assert result["text2"].segments == ["seg2"] - - -def test_get_existing_mappings_empty(mock_segment): - """Test getting mappings from segment with no mappings""" - # Arrange - segment = mock_segment( - id_str="12345678-1234-5678-1234-567812345678", - text_id="source", - content="content", - mapping=[] - ) - - # Act - result = _get_existing_mappings(segment) - - # Assert - assert isinstance(result, dict) - assert len(result) == 0 - - -def test_process_new_mappings_all_new(): - """Test processing entirely new mappings""" - # Arrange - new_mappings = [ - Mapping(text_id="text1", segments=["seg1"]), - Mapping(text_id="text2", segments=["seg2"]) - ] - existing_mappings: Dict[str, Mapping] = {} - - # Act - result = _process_new_mappings(new_mappings, existing_mappings) - - # Assert - assert len(result) == 2 - assert result == new_mappings - - -def test_process_new_mappings_merge(): - """Test processing mappings that need merging""" - # Arrange - existing_mappings = { - "text1": Mapping(text_id="text1", segments=["seg1", "seg2"]), - "text2": Mapping(text_id="text2", segments=["seg3"]) - } - new_mappings = [ - Mapping(text_id="text1", segments=["seg2", "seg4"]), - Mapping(text_id="text3", segments=["seg5"]) - ] - - # Act - result = _process_new_mappings(new_mappings, existing_mappings) - - # Assert - assert len(result) == 3 - text1_mapping = next(m for m in result if m.text_id == "text1") - assert set(text1_mapping.segments) == {"seg1", "seg2", "seg4"} - assert any(m.text_id == "text2" and m.segments == ["seg3"] for m in result) - assert any(m.text_id == "text3" and m.segments == ["seg5"] for m in result) - - -@pytest.mark.asyncio -async def test_construct_update_segments(mock_segment): - """Test construction of updated segments""" - # Arrange - segments = [ - mock_segment( - id_str="12345678-1234-5678-1234-567812345678", - text_id="source1", - content="content1", - mapping=[Mapping(text_id="text1", segments=["a", "b"])] - ), - mock_segment( - id_str="87654321-4321-8765-4321-876543210987", - text_id="source2", - content="content2", - mapping=[Mapping(text_id="text2", segments=["c"])] - ) - ] - update_dict = { - "12345678-1234-5678-1234-567812345678": [ - Mapping(text_id="text1", segments=["b", "d"]), - Mapping(text_id="text3", segments=["e"]) - ] - } - - # Act - result = await _construct_update_segments(segments, update_dict) - - # Assert - assert len(result) == 1 - updated_segment = result[0] - assert str(updated_segment.id) == "12345678-1234-5678-1234-567812345678" - assert len(updated_segment.mapping) == 2 - - text1_mapping = next(m for m in updated_segment.mapping if m.text_id == "text1") - assert set(text1_mapping.segments) == {"a", "b", "d"} - - text3_mapping = next(m for m in updated_segment.mapping if m.text_id == "text3") - assert text3_mapping.segments == ["e"] diff --git a/tests/texts/mappings/test_mappings_service.py b/tests/texts/mappings/test_mappings_service.py deleted file mode 100644 index 4fe0b5db8..000000000 --- a/tests/texts/mappings/test_mappings_service.py +++ /dev/null @@ -1,199 +0,0 @@ -from unittest.mock import AsyncMock, patch -import pytest -from fastapi import HTTPException, status -import uuid - -from pecha_api.error_contants import ErrorConstants -from pecha_api.texts.mappings.mappings_response_models import TextMappingRequest, MappingsModel, TextMapping -from pecha_api.texts.mappings.mappings_service import update_segment_mapping -from pecha_api.texts.segments.segments_models import Mapping -from pecha_api.texts.segments.segments_response_models import SegmentResponse -from pecha_api.texts.segments.segments_enum import SegmentType - -@pytest.mark.asyncio -async def test_update_segment_mapping_non_admin(): - """Test update fails when user is not admin""" - # Arrange - mapping_request = TextMappingRequest( - text_mappings=[ - TextMapping( - text_id="text-1", - segment_id="segment-1", - mappings=[MappingsModel(parent_text_id="parent-1", segments=["seg-1"])] - ) - ] - ) - - with patch('pecha_api.texts.mappings.mappings_service.verify_admin_access', return_value=False): - # Act & Assert - with pytest.raises(HTTPException) as exc_info: - await update_segment_mapping(text_mapping_request=mapping_request, token="Bearer token") - - assert exc_info.value.status_code == status.HTTP_403_FORBIDDEN - assert exc_info.value.detail == ErrorConstants.ADMIN_ERROR_MESSAGE - - -@pytest.mark.asyncio -async def test_update_segment_mapping_invalid_text(): - """Test update fails when text ID doesn't exist""" - # Arrange - text_id = "invalid-text" - segment_id = "f7e14876-a3af-4652-8c84-8df2c046a105" - parent_text_id = "c87aae38-ea7a-4d2b-ba0e-fd7dc61e68d1" - - mapping_request = TextMappingRequest( - text_mappings=[ - TextMapping( - text_id=text_id, - segment_id=segment_id, - mappings=[MappingsModel(parent_text_id=parent_text_id, segments=["seg-1"])] - ) - ] - ) - - # Create mock objects for valid IDs only (text_id is invalid) - mock_parent_text = AsyncMock() - mock_parent_text.id = uuid.UUID(parent_text_id) - mock_parent_text.pecha_text_id = parent_text_id - - mock_segment_obj = AsyncMock() - mock_segment_obj.id = uuid.UUID(segment_id) - mock_segment_obj.pecha_segment_id = segment_id - - with patch('pecha_api.texts.mappings.mappings_service.verify_admin_access', return_value=True), \ - patch('pecha_api.texts.mappings.mappings_service.get_texts_by_pecha_text_ids', new_callable=AsyncMock) as mock_get_texts, \ - patch('pecha_api.texts.mappings.mappings_service.get_segments_by_pecha_segment_ids', new_callable=AsyncMock) as mock_get_segments: - - # Return only valid texts (missing text_id) - mock_get_texts.return_value = [mock_parent_text] - mock_get_segments.return_value = [mock_segment_obj] - - # Act & Assert - with pytest.raises(KeyError): - await update_segment_mapping(text_mapping_request=mapping_request, token="Bearer token") - - -@pytest.mark.asyncio -async def test_update_segment_mapping_invalid_segment(): - """Test update fails when segment ID doesn't exist""" - # Arrange - text_id = "8749b360-a55e-441c-b541-f7c6ba2f3c61" - segment_id = "invalid-segment" - parent_text_id = "c87aae38-ea7a-4d2b-ba0e-fd7dc61e68d1" - - mapping_request = TextMappingRequest( - text_mappings=[ - TextMapping( - text_id=text_id, - segment_id=segment_id, - mappings=[MappingsModel(parent_text_id=parent_text_id, segments=["seg-1"])] - ) - ] - ) - - # Create mock text objects - mock_text = AsyncMock() - mock_text.id = uuid.UUID(text_id) - mock_text.pecha_text_id = text_id - - mock_parent_text = AsyncMock() - mock_parent_text.id = uuid.UUID(parent_text_id) - mock_parent_text.pecha_text_id = parent_text_id - - with patch('pecha_api.texts.mappings.mappings_service.verify_admin_access', return_value=True), \ - patch('pecha_api.texts.mappings.mappings_service.get_texts_by_pecha_text_ids', new_callable=AsyncMock) as mock_get_texts, \ - patch('pecha_api.texts.mappings.mappings_service.get_segments_by_pecha_segment_ids', new_callable=AsyncMock) as mock_get_segments: - - # Return texts but no segments (segment_id is invalid) - mock_get_texts.return_value = [mock_text, mock_parent_text] - mock_get_segments.return_value = [] - - # Act & Assert - with pytest.raises(KeyError): - await update_segment_mapping(text_mapping_request=mapping_request, token="Bearer token") - - -@pytest.mark.asyncio -async def test_update_segment_mapping_invalid_parent_text(): - """Test update fails when parent text ID doesn't exist""" - # Arrange - text_id = "8749b360-a55e-441c-b541-f7c6ba2f3c61" - segment_id = "f7e14876-a3af-4652-8c84-8df2c046a105" - invalid_parent_text_id = "invalid-parent" - - mapping_request = TextMappingRequest( - text_mappings=[ - TextMapping( - text_id=text_id, - segment_id=segment_id, - mappings=[MappingsModel(parent_text_id=invalid_parent_text_id, segments=["seg-1"])] - ) - ] - ) - - # Create mock text object - mock_text = AsyncMock() - mock_text.id = uuid.UUID(text_id) - mock_text.pecha_text_id = text_id - - # Create mock segment object - mock_segment_obj = AsyncMock() - mock_segment_obj.id = uuid.UUID(segment_id) - mock_segment_obj.pecha_segment_id = segment_id - - with patch('pecha_api.texts.mappings.mappings_service.verify_admin_access', return_value=True), \ - patch('pecha_api.texts.mappings.mappings_service.get_texts_by_pecha_text_ids', new_callable=AsyncMock) as mock_get_texts, \ - patch('pecha_api.texts.mappings.mappings_service.get_segments_by_pecha_segment_ids', new_callable=AsyncMock) as mock_get_segments: - - # Return only valid text (missing parent_text_id) - mock_get_texts.return_value = [mock_text] - mock_get_segments.return_value = [mock_segment_obj] - - # Act & Assert - with pytest.raises(KeyError): - await update_segment_mapping(text_mapping_request=mapping_request, token="Bearer token") - - -@pytest.mark.asyncio -async def test_update_segment_mapping_invalid_parent_segment(): - """Test update fails when parent segment ID doesn't exist""" - # Arrange - text_id = "8749b360-a55e-441c-b541-f7c6ba2f3c61" - segment_id = "f7e14876-a3af-4652-8c84-8df2c046a105" - parent_text_id = "c87aae38-ea7a-4d2b-ba0e-fd7dc61e68d1" - invalid_parent_segment = "invalid-seg" - - mapping_request = TextMappingRequest( - text_mappings=[ - TextMapping( - text_id=text_id, - segment_id=segment_id, - mappings=[MappingsModel(parent_text_id=parent_text_id, segments=[invalid_parent_segment])] - )] - ) - - # Create mock text objects - mock_text = AsyncMock() - mock_text.id = uuid.UUID(text_id) - mock_text.pecha_text_id = text_id - - mock_parent_text = AsyncMock() - mock_parent_text.id = uuid.UUID(parent_text_id) - mock_parent_text.pecha_text_id = parent_text_id - - # Create mock segment object - mock_segment_obj = AsyncMock() - mock_segment_obj.id = uuid.UUID(segment_id) - mock_segment_obj.pecha_segment_id = segment_id - - with patch('pecha_api.texts.mappings.mappings_service.verify_admin_access', return_value=True), \ - patch('pecha_api.texts.mappings.mappings_service.get_texts_by_pecha_text_ids', new_callable=AsyncMock) as mock_get_texts, \ - patch('pecha_api.texts.mappings.mappings_service.get_segments_by_pecha_segment_ids', new_callable=AsyncMock) as mock_get_segments: - - # Return texts and segment but not parent segment (invalid_parent_segment is missing) - mock_get_texts.return_value = [mock_text, mock_parent_text] - mock_get_segments.return_value = [mock_segment_obj] - - # Act & Assert - with pytest.raises(KeyError): - await update_segment_mapping(text_mapping_request=mapping_request, token="Bearer token") diff --git a/tests/texts/mappings/test_mappings_views.py b/tests/texts/mappings/test_mappings_views.py deleted file mode 100644 index b711a2d66..000000000 --- a/tests/texts/mappings/test_mappings_views.py +++ /dev/null @@ -1,142 +0,0 @@ -from unittest.mock import patch - -from fastapi.testclient import TestClient -from pecha_api.app import api -from fastapi import HTTPException - -from pecha_api.texts.segments.segments_response_models import SegmentResponse, MappingResponse,SegmentDTO -from pecha_api.texts.segments.segments_enum import SegmentType - -client = TestClient(api) - -text_mapping_request = { - "text_mappings": [ - { - "text_id": "2ff4215e-bc9e-4d16-8d7e-b4adea3c6ef9", - "segment_id": "cce14575-ebc3-43aa-bcce-777676f3b2e2", - "mappings": [ - { - "parent_text_id": "e55d66bc-0b2c-4575-afe1-c357856b1592", - "segments": [ - "5bbe24b9-625e-41bf-b6aa-a949f26a7c05", - "83311e49-7e8b-413d-95c3-80d2cdea5158" - ] - } - ] - } - ] -} - - -@patch("pecha_api.texts.mappings.mappings_views.update_segment_mapping") -def test_create_text_mapping(mock_update_mapping): - # Arrange - test_token = "test_token" - mapping_response = MappingResponse( - text_id="e55d66bc-0b2c-4575-afe1-c357856b1592", - segments=[ - "5bbe24b9-625e-41bf-b6aa-a949f26a7c05", - "83311e49-7e8b-413d-95c3-80d2cdea5158" - ] - ) - section_response = SegmentResponse( - segments=[ - SegmentDTO( - id="cce14575-ebc3-43aa-bcce-777676f3b2e2", - text_id="2ff4215e-bc9e-4d16-8d7e-b4adea3c6ef9", - content="content pf the segment", - type=SegmentType.SOURCE, - mapping=[mapping_response] - - ) - ] - ) - mock_update_mapping.return_value = section_response - response = client.post( - "/mappings", - headers={"Authorization": f"Bearer {test_token}"}, - json=text_mapping_request - ) - - # Assert - assert response.status_code == 201 - assert response.json() == section_response.model_dump(mode="json") - - -def test_create_text_mapping_403(): - # Act - response = client.post( - "/mappings", - json={"text_id": "123"} - ) - - # Assert - assert response.status_code == 403 - - -@patch("pecha_api.texts.mappings.mappings_views.update_segment_mapping") -def test_create_text_mapping_404_text_not_found(mock_update_mapping): - mock_update_mapping.side_effect = HTTPException( - status_code=404, - detail="Text not found" - ) - - response = client.post( - "/mappings", - headers={"Authorization": "Bearer token"}, - json=text_mapping_request - ) - - assert response.status_code == 404 - assert response.json()["detail"] == "Text not found" - - -@patch("pecha_api.texts.mappings.mappings_views.update_segment_mapping") -def test_create_text_mapping_404_segment_not_found(mock_update_mapping): - mock_update_mapping.side_effect = HTTPException( - status_code=404, - detail="Segment not found" - ) - - response = client.post( - "/mappings", - headers={"Authorization": "Bearer token"}, - json=text_mapping_request - ) - - assert response.status_code == 404 - assert response.json()["detail"] == "Segment not found" - - -@patch("pecha_api.texts.mappings.mappings_views.update_segment_mapping") -def test_create_text_mapping_404_parent_text_not_found(mock_update_mapping): - mock_update_mapping.side_effect = HTTPException( - status_code=404, - detail="Parent text not found" - ) - - response = client.post( - "/mappings", - headers={"Authorization": "Bearer token"}, - json=text_mapping_request - ) - - assert response.status_code == 404 - assert response.json()["detail"] == "Parent text not found" - - -@patch("pecha_api.texts.mappings.mappings_views.update_segment_mapping") -def test_create_text_mapping_404_parent_segment_not_found(mock_update_mapping): - mock_update_mapping.side_effect = HTTPException( - status_code=404, - detail="Parent segment not found" - ) - - response = client.post( - "/mappings", - headers={"Authorization": "Bearer token"}, - json=text_mapping_request - ) - - assert response.status_code == 404 - assert response.json()["detail"] == "Parent segment not found" diff --git a/tests/texts/test_text_cache_service.py b/tests/texts/test_text_cache_service.py index b1de68d62..011b466f1 100644 --- a/tests/texts/test_text_cache_service.py +++ b/tests/texts/test_text_cache_service.py @@ -2,391 +2,20 @@ from unittest.mock import patch, Mock, AsyncMock from pecha_api.texts.texts_cache_service import ( - get_text_details_cache, - set_text_details_cache, - get_text_by_text_id_or_collection_cache, - set_text_by_text_id_or_collection_cache, - get_table_of_contents_by_text_id_cache, - set_table_of_contents_by_text_id_cache, - get_text_versions_by_group_id_cache, - set_text_versions_by_group_id_cache, update_text_details_cache, invalidate_text_cache_on_update, - get_table_of_content_by_sheet_id_cache, - set_table_of_content_by_sheet_id_cache, - delete_table_of_content_by_sheet_id_cache, set_text_details_by_id_cache, get_text_details_by_id_cache, delete_text_details_by_id_cache ) from pecha_api.texts.texts_response_models import ( - DetailTableOfContent, - DetailSection, - DetailTextSegment, TextDTO, TableOfContent, TableOfContentType, - Section, - TextVersionResponse, - TextsCategoryResponse, - DetailTableOfContentResponse, - TableOfContentResponse ) -from pecha_api.collections.collections_response_models import CollectionModel from pecha_api.cache.cache_enums import CacheType -@pytest.mark.asyncio -async def test_get_text_details_cache_empty_cache(): - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=None): - - response = await get_text_details_cache(text_id="text_id", content_id="content_id", version_id="version_id", skip=0, limit=10, cache_type=CacheType.TEXT_DETAIL) - - assert response is None - -@pytest.mark.asyncio -async def test_set_text_details_cache_success(): - mock_text_detail = DetailTableOfContent( - id="id_1", - text_id="text_id_1", - sections=[ - DetailSection( - id=f"id_{i}", - title=f"section_{i}", - section_number=i, - parent_id="parent_id_1", - segments=[ - DetailTextSegment( - segment_id=f"segment_id_{i}", - segment_number=1, - content=f"content_{i}", - translation=None - ) - for i in range(1,6) - ], - sections=[], - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652" - ) - for i in range(1,6) - ] - ) - - with patch("pecha_api.texts.texts_cache_service.set_cache", new_callable=AsyncMock): - - await set_text_details_cache(text_id="text_id", content_id="content_id", version_id="version_id", skip=0, limit=10, data=mock_text_detail, cache_type=CacheType.TEXT_DETAIL) - - -@pytest.mark.asyncio -async def test_get_text_details_cache_for_text_details_response(): - mock_cache_data = DetailTableOfContent( - id="id_1", - text_id="text_id_1", - sections=[ - DetailSection( - id=f"id_{i}", - title=f"section_{i}", - section_number=i, - parent_id="parent_id_1", - segments=[ - DetailTextSegment( - segment_id=f"segment_id_{i}", - segment_number=1, - content=f"content_{i}", - translation=None - ) - for i in range(1,6) - ], - sections=[], - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652" - ) - for i in range(1,6) - ] - ) - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=mock_cache_data): - response = await get_text_details_cache(text_id="text_id", content_id="content_id", version_id="version_id", skip=0, limit=10, cache_type=CacheType.TEXT_DETAIL) - - assert response is not None - assert isinstance(response, DetailTableOfContent) - assert response.id == "id_1" - assert response.text_id == "text_id_1" - assert len(response.sections) == 5 - assert response.sections[0].id == "id_1" - -@pytest.mark.asyncio -async def test_set_text_details_cache_for_text_details_response(): - mock_cache_data = DetailTableOfContent( - id="id_1", - text_id="text_id_1", - sections=[ - DetailSection( - id=f"id_{i}", - title=f"section_{i}", - section_number=i, - parent_id="parent_id_1", - segments=[ - DetailTextSegment( - segment_id=f"segment_id_{i}", - segment_number=1, - content=f"content_{i}", - translation=None - ) - for i in range(1,6) - ], - sections=[], - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652" - ) - for i in range(1,6) - ] - ) - with patch("pecha_api.texts.texts_cache_service.set_cache", new_callable=AsyncMock): - await set_text_details_cache(text_id="text_id", content_id="content_id", version_id="version_id", skip=0, limit=10, data=mock_cache_data, cache_type=CacheType.TEXT_DETAIL) - -@pytest.mark.asyncio -async def test_get_text_by_text_id_or_collection_cache_empty_cache(): - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=None): - response = await get_text_by_text_id_or_collection_cache(text_id="text_id", collection_id="collection_id", language="en", skip=0, limit=10, cache_type=CacheType.TEXTS_BY_ID_OR_COLLECTION) - - - assert response is None - -@pytest.mark.asyncio -async def test_get_text_by_text_id_or_collection_cache_for_text_by_text_id_or_collection_response(): - mock_cache_data = TextDTO( - id="id_1", - title="title_1", - language="en", - group_id="group_id_1", - type="type_1", - is_published=True, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="published_by_1", - categories=[], - views=0 - ) - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=mock_cache_data): - - response = await get_text_by_text_id_or_collection_cache(text_id="text_id", collection_id="collection_id", language="en", skip=0, limit=10, cache_type=CacheType.TEXTS_BY_ID_OR_COLLECTION) - - - assert response is not None - assert isinstance(response, TextDTO) - assert response.id == "id_1" - -@pytest.mark.asyncio -async def test_set_text_by_text_id_or_collection_cache_for_text_by_text_id_or_collection_response(): - mock_cache_data = TextDTO( - id="id_1", - title="title_1", - language="en", - group_id="group_id_1", - type="type_1", - is_published=True, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="published_by_1", - categories=[], - views=0 - ) - - with patch("pecha_api.texts.texts_cache_service.set_cache", new_callable=AsyncMock): - await set_text_by_text_id_or_collection_cache(text_id="text_id", collection_id="collection_id", language="en", skip=0, limit=10, data=mock_cache_data, cache_type=CacheType.TEXTS_BY_ID_OR_COLLECTION) - -@pytest.mark.asyncio -async def test_get_table_of_contents_by_text_id_cache_empty_cache(): - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=None): - response = await get_table_of_contents_by_text_id_cache(text_id="text_id", language="en", skip=0, limit=10, cache_type=CacheType.TEXT_TABLE_OF_CONTENTS) - - assert response is None - -@pytest.mark.asyncio -async def test_get_table_of_contents_by_text_id_cache_for_table_of_contents_by_text_id_response(): - mock_cache_data = TableOfContent( - id="table_of_content_id", - text_id="text_id_1", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="id_1", - title="section_1", - section_number=1, - parent_id="id_1", - segments=[], - sections=[], - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652" - ) - ] - ) - - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=mock_cache_data): - - response = await get_table_of_contents_by_text_id_cache(text_id="text_id", language="en", skip=0, limit=10, cache_type=CacheType.TEXT_TABLE_OF_CONTENTS) - - assert response is not None - assert isinstance(response, TableOfContent) - assert response.id == "table_of_content_id" - assert response.text_id == "text_id_1" - assert len(response.sections) == 1 - assert response.sections[0].id == "id_1" - -@pytest.mark.asyncio -async def test_set_table_of_contents_by_text_id_cache_for_table_of_contents_by_text_id_response(): - mock_cache_data = TableOfContent( - id="table_of_content_id", - text_id="text_id_1", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="id_1", - title="section_1", - section_number=1, - parent_id="id_1", - segments=[], - sections=[], - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652" - ) - ] - ) - with patch("pecha_api.texts.texts_cache_service.set_cache", new_callable=AsyncMock): - await set_table_of_contents_by_text_id_cache(text_id="text_id", data=mock_cache_data, language="en", skip=0, limit=10, cache_type=CacheType.TEXT_TABLE_OF_CONTENTS) - -@pytest.mark.asyncio -async def test_get_text_versions_by_group_id_cache_empty_cache(): - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=None): - response = await get_text_versions_by_group_id_cache(text_id="text_id", language="en", skip=0, limit=10, cache_type=CacheType.TEXT_VERSIONS) - - assert response is None - -@pytest.mark.asyncio -async def test_get_text_versions_by_group_id_cache_for_text_versions_by_group_id_response(): - mock_cache_data = TextVersionResponse( - text=TextDTO( - id="id_1", - title="title_1", - language="en", - group_id="group_id_1", - type="type_1", - is_published=True, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="published_by_1", - categories=[], - views=0 - ), - versions=[] - ) - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=mock_cache_data): - response = await get_text_versions_by_group_id_cache(text_id="text_id", language="en", skip=0, limit=10, cache_type=CacheType.TEXT_VERSIONS) - - assert response is not None - assert isinstance(response, TextVersionResponse) - -@pytest.mark.asyncio -async def test_set_text_versions_by_group_id_cache_for_text_versions_by_group_id_response(): - mock_cache_data = TextVersionResponse( - text=TextDTO( - id="id_1", - title="title_1", - language="en", - group_id="group_id_1", - type="type_1", - is_published=True, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="published_by_1", - categories=[], - views=0 - ), - versions=[] - ) - with patch("pecha_api.texts.texts_cache_service.set_cache", new_callable=AsyncMock): - await set_text_versions_by_group_id_cache(text_id="text_id", language="en", skip=0, limit=10, data=mock_cache_data, cache_type=CacheType.TEXT_VERSIONS) - - -@pytest.mark.asyncio -async def test_set_text_by_text_id_or_collection_cache_success(): - mock_data = TextsCategoryResponse( - collection=CollectionModel( - id="id_1", - title="title_1", - description="description_1", - language="en", - slug="slug_1", - has_child=False - ), - texts=[], - total=0, - skip=0, - limit=10 - ) - - with patch("pecha_api.texts.texts_cache_service.set_cache", new_callable=AsyncMock): - - await set_text_by_text_id_or_collection_cache(text_id="text_id", collection_id="collection_id", language="en", skip=0, limit=10, data=mock_data, cache_type=CacheType.TEXTS_BY_ID_OR_COLLECTION) - -@pytest.mark.asyncio -async def test_set_table_of_contents_by_text_id_cache_success(): - mock_cache_data = TableOfContent( - id="table_of_content_id", - text_id="text_id_1", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="id_1", - title="section_1", - section_number=1, - parent_id="id_1", - segments=[], - sections=[], - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652" - ) - ] - ) - - with patch("pecha_api.texts.texts_cache_service.set_cache", new_callable=AsyncMock): - - await set_table_of_contents_by_text_id_cache(text_id="text_id", language="en", skip=0, limit=10, data=mock_cache_data, cache_type=CacheType.TEXT_TABLE_OF_CONTENTS) - -@pytest.mark.asyncio -async def test_set_text_versions_by_group_id_cache_success(): - mock_cache_data = TextVersionResponse( - text=TextDTO( - id="id_1", - title="title_1", - language="en", - group_id="group_id_1", - type="type_1", - is_published=True, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="published_by_1", - categories=[], - views=0 - ), - versions=[] - ) - - with patch("pecha_api.texts.texts_cache_service.set_cache", new_callable=AsyncMock): - - await set_text_versions_by_group_id_cache(text_id="text_id", language="en", skip=0, limit=10, data=mock_cache_data, cache_type=CacheType.TEXT_VERSIONS) - @pytest.mark.asyncio async def test_update_text_details_cache_success(): #Test successful update of text details cache @@ -563,186 +192,6 @@ def mock_generate_hash_key(payload): ] assert kwargs["hash_keys"] == expected_hash_keys -@pytest.mark.asyncio -async def test_get_text_details_cache_with_dict_response(): - #Test get_text_details_cache when cache returns dict and needs conversion# - mock_cache_dict = { - "text_detail": { - "id": "text_id_1", - "title": "Test Title", - "language": "en", - "group_id": "group_id_1", - "type": "root_text", - "is_published": True, - "created_date": "2025-03-16 04:40:54.757652", - "updated_date": "2025-03-16 04:40:54.757652", - "published_date": "2025-03-16 04:40:54.757652", - "published_by": "user_1", - "categories": [], - "views": 0 - }, - "content": { - "id": "id_1", - "text_id": "text_id_1", - "sections": [] - }, - "size": 20, - "pagination_direction": "next", - "current_segment_position": 1, - "total_segments": 100 - } - - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=mock_cache_dict): - response = await get_text_details_cache(text_id="text_id", content_id="content_id", version_id="version_id", skip=0, limit=10, cache_type=CacheType.TEXT_DETAIL) - - assert response is not None - assert isinstance(response, DetailTableOfContentResponse) - assert response.content.id == "id_1" - -@pytest.mark.asyncio -async def test_get_text_by_text_id_or_collection_cache_with_dict_response(): - #Test get_text_by_text_id_or_collection_cache when cache returns dict and needs conversion# - mock_cache_dict = { - "collection": { - "id": "id_1", - "title": "title_1", - "description": "description_1", - "language": "en", - "slug": "slug_1", - "has_child": False - }, - "texts": [], - "total": 0, - "skip": 0, - "limit": 10 - } - - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=mock_cache_dict): - response = await get_text_by_text_id_or_collection_cache(text_id="text_id", collection_id="collection_id", language="en", skip=0, limit=10, cache_type=CacheType.TEXTS_BY_ID_OR_COLLECTION) - - assert response is not None - assert isinstance(response, TextsCategoryResponse) - -@pytest.mark.asyncio -async def test_get_table_of_contents_by_text_id_cache_with_dict_response(): - #Test get_table_of_contents_by_text_id_cache when cache returns dict and needs conversion# - mock_cache_dict = { - "text_detail": { - "id": "text_id_1", - "title": "Test Title", - "language": "en", - "group_id": "group_id_1", - "type": "root_text", - "is_published": True, - "created_date": "2025-03-16 04:40:54.757652", - "updated_date": "2025-03-16 04:40:54.757652", - "published_date": "2025-03-16 04:40:54.757652", - "published_by": "user_1", - "categories": [], - "views": 0 - }, - "contents": [{ - "id": "table_of_content_id", - "text_id": "text_id_1", - "type": "text", - "sections": [] - }] - } - - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=mock_cache_dict): - response = await get_table_of_contents_by_text_id_cache(text_id="text_id", language="en", skip=0, limit=10, cache_type=CacheType.TEXT_TABLE_OF_CONTENTS) - - assert response is not None - assert isinstance(response, TableOfContentResponse) - assert response.contents[0].id == "table_of_content_id" - -@pytest.mark.asyncio -async def test_get_text_versions_by_group_id_cache_with_dict_response(): - #Test get_text_versions_by_group_id_cache when cache returns dict and needs conversion# - mock_cache_dict = { - "text": { - "id": "id_1", - "title": "title_1", - "language": "en", - "group_id": "group_id_1", - "type": "type_1", - "is_published": True, - "created_date": "2025-03-16 04:40:54.757652", - "updated_date": "2025-03-16 04:40:54.757652", - "published_date": "2025-03-16 04:40:54.757652", - "published_by": "published_by_1", - "categories": [], - "views": 0 - }, - "versions": [] - } - - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=mock_cache_dict): - response = await get_text_versions_by_group_id_cache(text_id="text_id", language="en", skip=0, limit=10, cache_type=CacheType.TEXT_VERSIONS) - - assert response is not None - assert isinstance(response, TextVersionResponse) - -@pytest.mark.asyncio -async def test_get_table_of_content_by_sheet_id_cache_empty_cache(): - #Test get_table_of_content_by_sheet_id_cache with empty cache# - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=None): - response = await get_table_of_content_by_sheet_id_cache(sheet_id="sheet_id", cache_type=CacheType.SHEET_TABLE_OF_CONTENT) - - assert response is None - -@pytest.mark.asyncio -async def test_get_table_of_content_by_sheet_id_cache_with_data(): - #Test get_table_of_content_by_sheet_id_cache with cached data# - mock_cache_data = TableOfContent( - id="table_of_content_id", - text_id="text_id_1", - type=TableOfContentType.SHEET, - sections=[] - ) - - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=mock_cache_data): - response = await get_table_of_content_by_sheet_id_cache(sheet_id="sheet_id", cache_type=CacheType.SHEET_TABLE_OF_CONTENT) - - assert response is not None - assert isinstance(response, TableOfContent) - assert response.id == "table_of_content_id" - -@pytest.mark.asyncio -async def test_get_table_of_content_by_sheet_id_cache_with_dict_response(): - #Test get_table_of_content_by_sheet_id_cache when cache returns dict and needs conversion# - mock_cache_dict = { - "id": "table_of_content_id", - "text_id": "text_id_1", - "type": "sheet", - "sections": [] - } - - with patch("pecha_api.texts.texts_cache_service.get_cache_data", new_callable=AsyncMock, return_value=mock_cache_dict): - response = await get_table_of_content_by_sheet_id_cache(sheet_id="sheet_id", cache_type=CacheType.SHEET_TABLE_OF_CONTENT) - - assert response is not None - assert isinstance(response, TableOfContent) - assert response.id == "table_of_content_id" - -@pytest.mark.asyncio -async def test_set_table_of_content_by_sheet_id_cache_success(): - #Test set_table_of_content_by_sheet_id_cache success# - mock_cache_data = TableOfContent( - id="table_of_content_id", - text_id="text_id_1", - type=TableOfContentType.SHEET, - sections=[] - ) - - with patch("pecha_api.texts.texts_cache_service.set_cache", new_callable=AsyncMock): - await set_table_of_content_by_sheet_id_cache(sheet_id="sheet_id", cache_type=CacheType.SHEET_TABLE_OF_CONTENT, data=mock_cache_data) - -@pytest.mark.asyncio -async def test_delete_table_of_content_by_sheet_id_cache_success(): - #Test delete_table_of_content_by_sheet_id_cache success# - with patch("pecha_api.texts.texts_cache_service.clear_cache", new_callable=AsyncMock): - await delete_table_of_content_by_sheet_id_cache(sheet_id="sheet_id", cache_type=CacheType.SHEET_TABLE_OF_CONTENT) @pytest.mark.asyncio async def test_set_text_details_by_id_cache_success(): @@ -828,112 +277,3 @@ async def test_delete_text_details_by_id_cache_success(): #Test delete_text_details_by_id_cache success# with patch("pecha_api.texts.texts_cache_service.clear_cache", new_callable=AsyncMock): await delete_text_details_by_id_cache(text_id="text_id", cache_type=CacheType.TEXT_DETAIL) - -@pytest.mark.asyncio -async def test_update_text_details_cache_for_sheet_success(): - #Test update_text_details_cache for sheet with successful table of content update# - updated_text_data = TextDTO( - id="sheet_id_1", - title="Updated Sheet Title", - language="en", - group_id="group_id_1", - type="sheet", - is_published=True, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 05:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="user_1", - categories=["category_1"], - views=10 - ) - - mock_table_of_content = TableOfContent( - id="toc_id", - text_id="sheet_id_1", - type=TableOfContentType.SHEET, - sections=[] - ) - - with patch("pecha_api.texts.texts_cache_service.update_cache", new_callable=AsyncMock, return_value=True) as mock_update_cache, \ - patch("pecha_api.texts.texts_cache_service.Utils.generate_hash_key", return_value="test_hash_key"), \ - patch("pecha_api.texts.texts_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, return_value=mock_table_of_content): - - result = await update_text_details_cache(text_id="sheet_id_1", updated_text_data=updated_text_data, cache_type=CacheType.SHEET_DETAIL) - - assert result is True - assert mock_update_cache.call_count == 3 # Called for primary cache, texts_by_id cache, and table of content cache - -@pytest.mark.asyncio -async def test_update_text_details_cache_for_sheet_with_toc_exception(): - #Test update_text_details_cache for sheet when table of content update raises exception# - updated_text_data = TextDTO( - id="sheet_id_1", - title="Updated Sheet Title", - language="en", - group_id="group_id_1", - type="sheet", - is_published=True, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 05:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="user_1", - categories=["category_1"], - views=10 - ) - - with patch("pecha_api.texts.texts_cache_service.update_cache", new_callable=AsyncMock, return_value=True) as mock_update_cache, \ - patch("pecha_api.texts.texts_cache_service.Utils.generate_hash_key", return_value="test_hash_key"), \ - patch("pecha_api.texts.texts_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, side_effect=Exception("TOC error")), \ - patch("pecha_api.texts.texts_cache_service.logging.warning") as mock_log_warning: - - result = await update_text_details_cache(text_id="sheet_id_1", updated_text_data=updated_text_data, cache_type=CacheType.SHEET_DETAIL) - - assert result is True - assert mock_update_cache.call_count == 2 # Called for primary cache and texts_by_id cache only - mock_log_warning.assert_called_once() - -@pytest.mark.asyncio -async def test_update_text_details_cache_for_sheet_with_none_toc(): - #Test update_text_details_cache for sheet when table of content returns None# - updated_text_data = TextDTO( - id="sheet_id_1", - title="Updated Sheet Title", - language="en", - group_id="group_id_1", - type="sheet", - is_published=True, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 05:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="user_1", - categories=["category_1"], - views=10 - ) - - with patch("pecha_api.texts.texts_cache_service.update_cache", new_callable=AsyncMock, return_value=True) as mock_update_cache, \ - patch("pecha_api.texts.texts_cache_service.Utils.generate_hash_key", return_value="test_hash_key"), \ - patch("pecha_api.texts.texts_service.get_table_of_content_by_sheet_id", new_callable=AsyncMock, return_value=None): - - result = await update_text_details_cache(text_id="sheet_id_1", updated_text_data=updated_text_data, cache_type=CacheType.SHEET_DETAIL) - - assert result is True - assert mock_update_cache.call_count == 2 # Called for primary cache and texts_by_id cache only - -@pytest.mark.asyncio -async def test_invalidate_text_cache_on_update_for_sheet(): - #Test invalidate_text_cache_on_update for sheet with sheet-specific cache types# - with patch("pecha_api.texts.texts_cache_service.invalidate_multiple_cache_keys", new_callable=AsyncMock) as mock_invalidate_multiple, \ - patch("pecha_api.texts.texts_cache_service.invalidate_text_related_cache", new_callable=AsyncMock) as mock_invalidate_text, \ - patch("pecha_api.texts.texts_cache_service.Utils.generate_hash_key", return_value="test_hash_key"): - - result = await invalidate_text_cache_on_update(text_id="sheet_id_1", cache_type=CacheType.SHEET_DETAIL) - - assert result is True - mock_invalidate_multiple.assert_called_once() - mock_invalidate_text.assert_called_once_with(text_id="sheet_id_1") - - # Verify that hash keys are generated for sheet-specific cache types - args, kwargs = mock_invalidate_multiple.call_args - assert "hash_keys" in kwargs - assert len(kwargs["hash_keys"]) == 4 # Four different cache types for sheets - diff --git a/tests/texts/test_text_repository.py b/tests/texts/test_text_repository.py deleted file mode 100644 index 090bfec49..000000000 --- a/tests/texts/test_text_repository.py +++ /dev/null @@ -1,261 +0,0 @@ -from unittest.mock import AsyncMock, patch, MagicMock -import pytest -from uuid import uuid4 - -from pecha_api.texts.texts_repository import fetch_sheets_from_db -from pecha_api.texts.texts_models import Text -from pecha_api.texts.texts_enums import TextType -from pecha_api.sheets.sheets_enum import SortBy, SortOrder - - -@pytest.mark.asyncio -async def test_fetch_sheets_from_db_default_parameters(): - """Test fetch_sheets_from_db with default parameters""" - # Mock Text objects - mock_text_1 = MagicMock() - mock_text_1.id = str(uuid4()) - mock_text_1.title = "Test Sheet 1" - mock_text_1.language = "bo" - mock_text_1.group_id = "group_1" - mock_text_1.type = TextType.SHEET - mock_text_1.is_published = True - mock_text_1.created_date = "2024-01-01 10:00:00" - mock_text_1.updated_date = "2024-01-01 10:00:00" - mock_text_1.published_date = "2024-01-01 10:00:00" - mock_text_1.published_by = "test@example.com" - mock_text_1.categories = [] - mock_text_1.views = 100 - - mock_text_2 = MagicMock() - mock_text_2.id = str(uuid4()) - mock_text_2.title = "Test Sheet 2" - mock_text_2.language = "en" - mock_text_2.group_id = "group_2" - mock_text_2.type = TextType.SHEET - mock_text_2.is_published = True - mock_text_2.created_date = "2024-01-02 15:30:00" - mock_text_2.updated_date = "2024-01-02 15:30:00" - mock_text_2.published_date = "2024-01-02 15:30:00" - mock_text_2.published_by = "another@example.com" - mock_text_2.categories = [] - mock_text_2.views = 250 - - mock_sheets = [mock_text_1, mock_text_2] - - with patch.object(Text, 'get_sheets', new_callable=AsyncMock, return_value=mock_sheets) as mock_get_sheets: - result = await fetch_sheets_from_db() - - # Verify the result - assert result is not None - assert isinstance(result, list) - assert len(result) == 2 - assert result == mock_sheets - - # Verify Text.get_sheets was called with correct default parameters - mock_get_sheets.assert_called_once_with( - published_by=None, - is_published=None, - sort_by=None, - sort_order=None, - skip=0, - limit=10 - ) - - -@pytest.mark.asyncio -async def test_fetch_sheets_from_db_with_all_parameters(): - """Test fetch_sheets_from_db with all parameters specified""" - mock_sheets = [] - - with patch.object(Text, 'get_sheets', new_callable=AsyncMock, return_value=mock_sheets) as mock_get_sheets: - result = await fetch_sheets_from_db( - published_by="test@example.com", - is_published=True, - sort_by=SortBy.PUBLISHED_DATE, - sort_order=SortOrder.DESC, - skip=20, - limit=5 - ) - - # Verify the result - assert result is not None - assert isinstance(result, list) - assert len(result) == 0 - assert result == mock_sheets - - # Verify Text.get_sheets was called with correct parameters - mock_get_sheets.assert_called_once_with( - published_by="test@example.com", - is_published=True, - sort_by=SortBy.PUBLISHED_DATE, - sort_order=SortOrder.DESC, - skip=20, - limit=5 - ) - - -@pytest.mark.asyncio -async def test_fetch_sheets_from_db_with_partial_parameters(): - """Test fetch_sheets_from_db with partial parameters""" - mock_text = MagicMock() - mock_text.id = str(uuid4()) - mock_text.title = "Filtered Sheet" - mock_text.language = "bo" - mock_text.group_id = "group_1" - mock_text.type = TextType.SHEET - mock_text.is_published = False - mock_text.created_date = "2024-01-01 10:00:00" - mock_text.updated_date = "2024-01-01 10:00:00" - mock_text.published_date = "2024-01-01 10:00:00" - mock_text.published_by = "specific@example.com" - mock_text.categories = [] - mock_text.views = 50 - - mock_sheets = [mock_text] - - with patch.object(Text, 'get_sheets', new_callable=AsyncMock, return_value=mock_sheets) as mock_get_sheets: - result = await fetch_sheets_from_db( - published_by="specific@example.com", - is_published=False, - skip=5 - ) - - # Verify the result - assert result is not None - assert isinstance(result, list) - assert len(result) == 1 - assert result[0] == mock_text - - # Verify Text.get_sheets was called with correct parameters - mock_get_sheets.assert_called_once_with( - published_by="specific@example.com", - is_published=False, - sort_by=None, - sort_order=None, - skip=5, - limit=10 - ) - - -@pytest.mark.asyncio -async def test_fetch_sheets_from_db_empty_result(): - """Test fetch_sheets_from_db when no sheets are found""" - with patch.object(Text, 'get_sheets', new_callable=AsyncMock, return_value=[]) as mock_get_sheets: - result = await fetch_sheets_from_db() - - # Verify the result - assert result is not None - assert isinstance(result, list) - assert len(result) == 0 - - # Verify Text.get_sheets was called - mock_get_sheets.assert_called_once() - - -@pytest.mark.asyncio -async def test_fetch_sheets_from_db_with_sort_by_created_date(): - """Test fetch_sheets_from_db with sort by created date""" - mock_sheets = [] - - with patch.object(Text, 'get_sheets', new_callable=AsyncMock, return_value=mock_sheets) as mock_get_sheets: - result = await fetch_sheets_from_db( - sort_by=SortBy.CREATED_DATE, - sort_order=SortOrder.ASC - ) - - # Verify the result - assert result is not None - assert isinstance(result, list) - - # Verify Text.get_sheets was called with correct sorting parameters - mock_get_sheets.assert_called_once_with( - published_by=None, - is_published=None, - sort_by=SortBy.CREATED_DATE, - sort_order=SortOrder.ASC, - skip=0, - limit=10 - ) - - -@pytest.mark.asyncio -async def test_fetch_sheets_from_db_large_skip_and_limit(): - """Test fetch_sheets_from_db with large skip and limit values""" - mock_sheets = [] - - with patch.object(Text, 'get_sheets', new_callable=AsyncMock, return_value=mock_sheets) as mock_get_sheets: - result = await fetch_sheets_from_db(skip=100, limit=50) - - # Verify the result - assert result is not None - assert isinstance(result, list) - - # Verify Text.get_sheets was called with correct pagination parameters - mock_get_sheets.assert_called_once_with( - published_by=None, - is_published=None, - sort_by=None, - sort_order=None, - skip=100, - limit=50 - ) - - -@pytest.mark.asyncio -async def test_fetch_sheets_from_db_published_only(): - """Test fetch_sheets_from_db filtering for published sheets only""" - mock_text = MagicMock() - mock_text.id = str(uuid4()) - mock_text.title = "Published Sheet" - mock_text.is_published = True - mock_text.type = TextType.SHEET - - mock_sheets = [mock_text] - - with patch.object(Text, 'get_sheets', new_callable=AsyncMock, return_value=mock_sheets) as mock_get_sheets: - result = await fetch_sheets_from_db(is_published=True) - - # Verify the result - assert result is not None - assert isinstance(result, list) - assert len(result) == 1 - - # Verify Text.get_sheets was called with is_published=True - mock_get_sheets.assert_called_once_with( - published_by=None, - is_published=True, - sort_by=None, - sort_order=None, - skip=0, - limit=10 - ) - - -@pytest.mark.asyncio -async def test_fetch_sheets_from_db_unpublished_only(): - """Test fetch_sheets_from_db filtering for unpublished sheets only""" - mock_text = MagicMock() - mock_text.id = str(uuid4()) - mock_text.title = "Draft Sheet" - mock_text.is_published = False - mock_text.type = TextType.SHEET - - mock_sheets = [mock_text] - - with patch.object(Text, 'get_sheets', new_callable=AsyncMock, return_value=mock_sheets) as mock_get_sheets: - result = await fetch_sheets_from_db(is_published=False) - - # Verify the result - assert result is not None - assert isinstance(result, list) - assert len(result) == 1 - - # Verify Text.get_sheets was called with is_published=False - mock_get_sheets.assert_called_once_with( - published_by=None, - is_published=False, - sort_by=None, - sort_order=None, - skip=0, - limit=10 - ) \ No newline at end of file diff --git a/tests/texts/test_texts_openpecha_service.py b/tests/texts/test_texts_openpecha_service.py index f3892b847..01fad76a8 100644 --- a/tests/texts/test_texts_openpecha_service.py +++ b/tests/texts/test_texts_openpecha_service.py @@ -6,9 +6,12 @@ _extract_title, _map_external_text_to_dto, _get_texts_by_collection_id, + _fetch_versions_from_parent, + _fetch_versions_from_related, + _fetch_commentaries_from_parent, + _fetch_commentaries_from_related, map_external_text_to_dto, map_external_text_to_text_version, - filter_versions_by_language, paginate_versions, fetch_translation_details, fetch_commentary_details, @@ -87,6 +90,10 @@ def test_extract_title_none_language(self): result = _extract_title(title_payload, None) assert result == "English Title" + def test_extract_title_invalid_payload_returns_empty_string(self): + assert _extract_title(None) == "" + assert _extract_title(123) == "" + class TestMapExternalTextToDto: """Tests for map_external_text_to_dto function (legacy TextDTO mapper).""" @@ -168,70 +175,6 @@ def test_map_commentary_data(self): assert result.parent_id == "text-123" -class TestFilterVersionsByLanguage: - """Tests for filter_versions_by_language function.""" - - def test_filter_with_language(self): - versions = [ - TextVersion( - id="v1", title="English", parent_id=None, priority=1, - language="en", type="translation", group_id="g1", - table_of_contents=[], is_published=True, - created_date="2025-01-01", updated_date="2025-01-01", - published_date="2025-01-01", published_by="user" - ), - TextVersion( - id="v2", title="Tibetan", parent_id=None, priority=2, - language="bo", type="translation", group_id="g1", - table_of_contents=[], is_published=True, - created_date="2025-01-01", updated_date="2025-01-01", - published_date="2025-01-01", published_by="user" - ) - ] - - result = filter_versions_by_language(versions, "en") - - assert len(result) == 1 - assert result[0].language == "en" - - def test_filter_without_language(self): - versions = [ - TextVersion( - id="v1", title="English", parent_id=None, priority=1, - language="en", type="translation", group_id="g1", - table_of_contents=[], is_published=True, - created_date="2025-01-01", updated_date="2025-01-01", - published_date="2025-01-01", published_by="user" - ), - TextVersion( - id="v2", title="Tibetan", parent_id=None, priority=2, - language="bo", type="translation", group_id="g1", - table_of_contents=[], is_published=True, - created_date="2025-01-01", updated_date="2025-01-01", - published_date="2025-01-01", published_by="user" - ) - ] - - result = filter_versions_by_language(versions, None) - - assert len(result) == 2 - - def test_filter_no_matches(self): - versions = [ - TextVersion( - id="v1", title="English", parent_id=None, priority=1, - language="en", type="translation", group_id="g1", - table_of_contents=[], is_published=True, - created_date="2025-01-01", updated_date="2025-01-01", - published_date="2025-01-01", published_by="user" - ) - ] - - result = filter_versions_by_language(versions, "zh") - - assert len(result) == 0 - - class TestPaginateVersions: """Tests for paginate_versions function.""" @@ -293,10 +236,35 @@ async def test_passes_skip_and_limit_to_upstream_without_language(self, mock_fet mock_fetch_texts.assert_awaited_once_with( category_id="cat-1", + title=None, offset=5, limit=3, ) + @pytest.mark.asyncio + @patch("pecha_api.texts.texts_openpecha_service.fetch_texts_by_category", new_callable=AsyncMock) + async def test_passes_title_filter_to_upstream(self, mock_fetch_texts): + mock_fetch_texts.return_value = { + "items": [_text_item("t-en", "en")], + "has_more": False, + } + + texts, has_more = await _get_texts_by_collection_id( + collection_id="cat-1", + skip=0, + limit=10, + title="heart", + ) + + assert len(texts) == 1 + assert has_more is False + mock_fetch_texts.assert_awaited_once_with( + category_id="cat-1", + title="heart", + offset=0, + limit=10, + ) + @pytest.mark.asyncio @patch("pecha_api.texts.texts_openpecha_service.fetch_texts_by_category", new_callable=AsyncMock) async def test_returns_empty_list_when_no_items(self, mock_fetch_texts): @@ -376,6 +344,32 @@ async def test_get_texts_success(self, mock_fetch_texts, mock_fetch_category): mock_fetch_texts.assert_awaited_once_with( category_id="cat-1", + title=None, + offset=0, + limit=10, + ) + + @pytest.mark.asyncio + @patch("pecha_api.texts.texts_openpecha_service.fetch_category_by_id", new_callable=AsyncMock) + @patch("pecha_api.texts.texts_openpecha_service.fetch_texts_by_category", new_callable=AsyncMock) + async def test_get_texts_with_title_filter(self, mock_fetch_texts, mock_fetch_category): + mock_fetch_texts.return_value = { + "items": [{"id": "t-en", "title": {"en": "Heart Sutra"}, "language": "en"}], + "has_more": False, + } + mock_fetch_category.return_value = {"title": {"en": "Collection"}} + + result = await get_texts_by_collection_from_openpecha( + collection_id="cat-1", + title="heart", + skip=0, + limit=10, + ) + + assert len(result.texts) == 1 + mock_fetch_texts.assert_awaited_once_with( + category_id="cat-1", + title="heart", offset=0, limit=10, ) @@ -406,6 +400,7 @@ async def test_get_texts_with_pagination(self, mock_fetch_texts, mock_fetch_cate mock_fetch_texts.assert_awaited_once_with( category_id="cat-1", + title=None, offset=1, limit=1, ) @@ -433,6 +428,27 @@ async def test_get_texts_category_upstream_error(self, mock_fetch_texts, mock_fe assert exc_info.value.status_code == 502 assert "category" in exc_info.value.detail.lower() + @pytest.mark.asyncio + @patch("pecha_api.texts.texts_openpecha_service.fetch_category_by_id", new_callable=AsyncMock) + @patch("pecha_api.texts.texts_openpecha_service.fetch_texts_by_category", new_callable=AsyncMock) + async def test_get_texts_without_collection_id(self, mock_fetch_texts, mock_fetch_category): + mock_fetch_texts.return_value = { + "items": [{"id": "t1", "title": {"en": "Text 1"}, "language": "en"}], + "has_more": False, + } + + result = await get_texts_by_collection_from_openpecha(skip=0, limit=10) + + assert result.collection is None + assert len(result.texts) == 1 + mock_fetch_texts.assert_awaited_once_with( + category_id=None, + title=None, + offset=0, + limit=10, + ) + mock_fetch_category.assert_not_awaited() + # ============================================================================= # Service Function Tests - get_text_by_id_from_openpecha (V2) @@ -489,12 +505,26 @@ class TestGetTextVersionsFromOpenpecha: @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') @patch('pecha_api.texts.texts_openpecha_service.fetch_translation_details') async def test_get_text_versions_success(self, mock_fetch_translations, mock_fetch_text): - mock_fetch_text.return_value = MOCK_EXTERNAL_TEXT_DATA + """Test default behavior when commentary_of is set (skips related text lookup).""" + # Text with commentary_of set - goes to default behavior + text_data = { + "id": "text-123", + "bdrc": "bdrc-123", + "title": {"en": "Heart Sutra", "bo": "ཤེས་རབ་སྙིང་པོ།"}, + "language": "bo", + "category_id": "cat-1", + "date": "2025-01-01", + "license": "CC0", + "commentary_of": "parent-text", # Has commentary_of, so default behavior applies + "translation_of": None, + "translations": ["trans-1", "trans-2"], + "commentaries": ["comm-1"] + } + mock_fetch_text.return_value = text_data mock_fetch_translations.return_value = [MOCK_TRANSLATION_DATA] result = await get_text_versions_from_openpecha( text_id="text-123", - language=None, skip=0, limit=10 ) @@ -513,7 +543,6 @@ async def test_get_text_versions_text_not_found(self, mock_fetch_text): with pytest.raises(HTTPException) as exc_info: await get_text_versions_from_openpecha( text_id="nonexistent", - language=None, skip=0, limit=10 ) @@ -529,7 +558,6 @@ async def test_get_text_versions_upstream_error(self, mock_fetch_text): with pytest.raises(HTTPException) as exc_info: await get_text_versions_from_openpecha( text_id="text-123", - language=None, skip=0, limit=10 ) @@ -549,7 +577,6 @@ async def test_get_text_versions_no_translations(self, mock_fetch_text): result = await get_text_versions_from_openpecha( text_id="text-123", - language=None, skip=0, limit=10 ) @@ -560,42 +587,169 @@ async def test_get_text_versions_no_translations(self, mock_fetch_text): @pytest.mark.asyncio @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') @patch('pecha_api.texts.texts_openpecha_service.fetch_translation_details') - async def test_get_text_versions_with_language_filter(self, mock_fetch_translations, mock_fetch_text): + async def test_get_text_versions_with_pagination(self, mock_fetch_translations, mock_fetch_text): mock_fetch_text.return_value = MOCK_EXTERNAL_TEXT_DATA mock_fetch_translations.return_value = [ - {"id": "t1", "title": {"en": "English"}, "language": "en", "translation_of": "text-123"}, - {"id": "t2", "title": {"bo": "Tibetan"}, "language": "bo", "translation_of": "text-123"} + {"id": f"t{i}", "title": {"en": f"Trans {i}"}, "language": "en", "translation_of": "text-123"} + for i in range(5) ] result = await get_text_versions_from_openpecha( text_id="text-123", - language="en", - skip=0, - limit=10 + skip=2, + limit=2 ) - assert len(result.versions) == 1 - assert result.versions[0].language == "en" + assert len(result.versions) == 2 @pytest.mark.asyncio @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') @patch('pecha_api.texts.texts_openpecha_service.fetch_translation_details') - async def test_get_text_versions_with_pagination(self, mock_fetch_translations, mock_fetch_text): - mock_fetch_text.return_value = MOCK_EXTERNAL_TEXT_DATA + async def test_get_text_versions_with_translation_of(self, mock_fetch_translations, mock_fetch_text): + """When translation_of is not null, fetch versions from the parent text.""" + # First call: the requested text has translation_of pointing to parent + translation_text = { + "id": "trans-1", + "title": {"en": "Translation"}, + "language": "en", + "translation_of": "root-text-123", + "commentary_of": None, + "translations": [], + "commentaries": [] + } + # Second call: the parent text has translations + parent_text = { + "id": "root-text-123", + "title": {"bo": "Root Text"}, + "language": "bo", + "translations": ["trans-1", "trans-2"], + "commentaries": [] + } + mock_fetch_text.side_effect = [translation_text, parent_text] mock_fetch_translations.return_value = [ - {"id": f"t{i}", "title": {"en": f"Trans {i}"}, "language": "en", "translation_of": "text-123"} - for i in range(5) + {"id": "trans-1", "title": {"en": "Translation 1"}, "language": "en", "translation_of": "root-text-123"}, + {"id": "trans-2", "title": {"en": "Translation 2"}, "language": "zh", "translation_of": "root-text-123"} ] - result = await get_text_versions_from_openpecha( - text_id="text-123", - language=None, - skip=2, - limit=2 - ) + result = await get_text_versions_from_openpecha(text_id="trans-1", skip=0, limit=10) + + assert result.text.id == "trans-1" + assert len(result.versions) == 2 + assert result.versions[0].id == "trans-1" + assert result.versions[1].id == "trans-2" + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + @patch('pecha_api.texts.texts_openpecha_service.fetch_translation_details') + async def test_get_text_versions_both_null_with_translations_list(self, mock_fetch_translations, mock_fetch_text): + """When both commentary_of and translation_of are null, check translations list.""" + # Root text with translations list + root_text = { + "id": "root-123", + "title": {"bo": "Root Text"}, + "language": "bo", + "translation_of": None, + "commentary_of": None, + "translations": ["trans-1"], + "commentaries": [] + } + # The translation has translation_of pointing to a parent + translation_text = { + "id": "trans-1", + "title": {"en": "Translation"}, + "language": "en", + "translation_of": "root-123", + "translations": [], + "commentaries": [] + } + # Parent text (root-123) has translations + parent_text = { + "id": "root-123", + "title": {"bo": "Root Text"}, + "language": "bo", + "translations": ["trans-1", "trans-2"], + "commentaries": [] + } + mock_fetch_text.side_effect = [root_text, translation_text, parent_text] + mock_fetch_translations.return_value = [ + {"id": "trans-1", "title": {"en": "Translation 1"}, "language": "en"}, + {"id": "trans-2", "title": {"en": "Translation 2"}, "language": "zh"} + ] + result = await get_text_versions_from_openpecha(text_id="root-123", skip=0, limit=10) + + assert result.text.id == "root-123" assert len(result.versions) == 2 + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + async def test_get_text_versions_translation_of_parent_not_found(self, mock_fetch_text): + """When translation_of parent is not found, return empty versions.""" + translation_text = { + "id": "trans-1", + "title": {"en": "Translation"}, + "language": "en", + "translation_of": "missing-parent", + "commentary_of": None + } + mock_fetch_text.side_effect = [translation_text, None] + + result = await get_text_versions_from_openpecha(text_id="trans-1", skip=0, limit=10) + + assert result.text.id == "trans-1" + assert len(result.versions) == 0 + + +# ============================================================================= +# Service Function Tests - _fetch_versions_from_parent +# ============================================================================= + +class TestFetchVersionsFromParent: + """Tests for _fetch_versions_from_parent helper function.""" + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + @patch('pecha_api.texts.texts_openpecha_service.fetch_translation_details') + async def test_fetch_versions_from_parent_success(self, mock_fetch_translations, mock_fetch_text): + original_text = MagicMock(spec=TextDTO) + original_text.id = "trans-1" + parent_data = { + "id": "root-123", + "translations": ["trans-1", "trans-2"] + } + mock_fetch_text.return_value = parent_data + mock_fetch_translations.return_value = [ + {"id": "trans-1", "title": {"en": "Trans 1"}, "language": "en"}, + {"id": "trans-2", "title": {"en": "Trans 2"}, "language": "zh"} + ] + + result = await _fetch_versions_from_parent("root-123", original_text, 0, 10) + + assert result.text == original_text + assert len(result.versions) == 2 + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + async def test_fetch_versions_from_parent_not_found(self, mock_fetch_text): + original_text = MagicMock(spec=TextDTO) + mock_fetch_text.return_value = None + + result = await _fetch_versions_from_parent("missing", original_text, 0, 10) + + assert result.text == original_text + assert len(result.versions) == 0 + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + async def test_fetch_versions_from_parent_error(self, mock_fetch_text): + original_text = MagicMock(spec=TextDTO) + mock_fetch_text.side_effect = Exception("Connection error") + + result = await _fetch_versions_from_parent("root-123", original_text, 0, 10) + + assert result.text == original_text + assert len(result.versions) == 0 + # ============================================================================= # Service Function Tests - get_text_commentaries_from_openpecha @@ -781,6 +935,227 @@ async def test_get_commentaries_partial_fetch_failure(self, mock_fetch_commentar assert len(result) == 1 assert result[0].id == "c1" + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + @patch('pecha_api.texts.texts_openpecha_service.fetch_commentary_details') + async def test_get_commentaries_with_commentary_of(self, mock_fetch_commentaries, mock_fetch_text): + """When commentary_of is not null, fetch commentaries from the parent text.""" + # First call: the requested text has commentary_of pointing to parent + commentary_text = { + "id": "comm-1", + "title": {"en": "Commentary"}, + "language": "bo", + "commentary_of": "root-text-123", + "translation_of": None, + "translations": [], + "commentaries": [] + } + # Second call: the parent text has commentaries + parent_text = { + "id": "root-text-123", + "title": {"bo": "Root Text"}, + "language": "bo", + "commentaries": ["comm-1", "comm-2"] + } + mock_fetch_text.side_effect = [commentary_text, parent_text] + mock_fetch_commentaries.return_value = [ + {"id": "comm-1", "title": {"en": "Commentary 1"}, "language": "bo"}, + {"id": "comm-2", "title": {"en": "Commentary 2"}, "language": "bo"} + ] + + result = await get_text_commentaries_from_openpecha(text_id="comm-1", skip=0, limit=10) + + assert len(result) == 2 + assert result[0].id == "comm-1" + assert result[1].id == "comm-2" + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + @patch('pecha_api.texts.texts_openpecha_service.fetch_commentary_details') + async def test_get_commentaries_both_null_with_translations_list(self, mock_fetch_commentaries, mock_fetch_text): + """When both commentary_of and translation_of are null, check translations/commentaries list.""" + # Root text with translations list + root_text = { + "id": "root-123", + "title": {"bo": "Root Text"}, + "language": "bo", + "translation_of": None, + "commentary_of": None, + "translations": ["trans-1"], + "commentaries": [] + } + # The translation has commentary_of pointing to a parent + translation_text = { + "id": "trans-1", + "title": {"en": "Translation"}, + "language": "en", + "commentary_of": "root-123", + "translations": [], + "commentaries": [] + } + # Parent text (root-123) has commentaries + parent_text = { + "id": "root-123", + "title": {"bo": "Root Text"}, + "language": "bo", + "commentaries": ["comm-1", "comm-2"] + } + mock_fetch_text.side_effect = [root_text, translation_text, parent_text] + mock_fetch_commentaries.return_value = [ + {"id": "comm-1", "title": {"en": "Commentary 1"}, "language": "bo"}, + {"id": "comm-2", "title": {"en": "Commentary 2"}, "language": "bo"} + ] + + result = await get_text_commentaries_from_openpecha(text_id="root-123", skip=0, limit=10) + + assert len(result) == 2 + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + async def test_get_commentaries_commentary_of_parent_not_found(self, mock_fetch_text): + """When commentary_of parent is not found, return empty commentaries.""" + commentary_text = { + "id": "comm-1", + "title": {"en": "Commentary"}, + "language": "bo", + "commentary_of": "missing-parent", + "translation_of": None + } + mock_fetch_text.side_effect = [commentary_text, None] + + result = await get_text_commentaries_from_openpecha(text_id="comm-1", skip=0, limit=10) + + assert len(result) == 0 + + +# ============================================================================= +# Service Function Tests - _fetch_commentaries_from_parent +# ============================================================================= + +class TestFetchCommentariesFromParent: + """Tests for _fetch_commentaries_from_parent helper function.""" + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + @patch('pecha_api.texts.texts_openpecha_service.fetch_commentary_details') + async def test_fetch_commentaries_from_parent_success(self, mock_fetch_commentaries, mock_fetch_text): + parent_data = { + "id": "root-123", + "commentaries": ["comm-1", "comm-2"] + } + mock_fetch_text.return_value = parent_data + mock_fetch_commentaries.return_value = [ + {"id": "comm-1", "title": {"en": "Commentary 1"}, "language": "bo"}, + {"id": "comm-2", "title": {"en": "Commentary 2"}, "language": "bo"} + ] + + result = await _fetch_commentaries_from_parent("root-123", 0, 10) + + assert len(result) == 2 + assert result[0].id == "comm-1" + assert result[1].id == "comm-2" + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + async def test_fetch_commentaries_from_parent_not_found(self, mock_fetch_text): + mock_fetch_text.return_value = None + + result = await _fetch_commentaries_from_parent("missing", 0, 10) + + assert len(result) == 0 + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + async def test_fetch_commentaries_from_parent_error(self, mock_fetch_text): + mock_fetch_text.side_effect = Exception("Connection error") + + result = await _fetch_commentaries_from_parent("root-123", 0, 10) + + assert len(result) == 0 + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + async def test_fetch_commentaries_from_parent_no_commentaries(self, mock_fetch_text): + parent_data = { + "id": "root-123", + "commentaries": [] + } + mock_fetch_text.return_value = parent_data + + result = await _fetch_commentaries_from_parent("root-123", 0, 10) + + assert len(result) == 0 + + +# ============================================================================= +# Service Function Tests - _fetch_commentaries_from_related +# ============================================================================= + +class TestFetchCommentariesFromRelated: + """Tests for _fetch_commentaries_from_related helper function.""" + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + @patch('pecha_api.texts.texts_openpecha_service.fetch_commentary_details') + async def test_fetch_commentaries_from_related_with_commentary_of(self, mock_fetch_commentaries, mock_fetch_text): + """When related text has commentary_of, fetch from that parent.""" + related_data = { + "id": "trans-1", + "commentary_of": "root-123", + "commentaries": [] + } + parent_data = { + "id": "root-123", + "commentaries": ["comm-1"] + } + mock_fetch_text.side_effect = [related_data, parent_data] + mock_fetch_commentaries.return_value = [ + {"id": "comm-1", "title": {"en": "Commentary"}, "language": "bo"} + ] + + result = await _fetch_commentaries_from_related("trans-1", 0, 10) + + assert len(result) == 1 + assert result[0].id == "comm-1" + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + @patch('pecha_api.texts.texts_openpecha_service.fetch_commentary_details') + async def test_fetch_commentaries_from_related_direct(self, mock_fetch_commentaries, mock_fetch_text): + """When related text has no commentary_of, fetch its own commentaries.""" + related_data = { + "id": "trans-1", + "commentary_of": None, + "commentaries": ["comm-1", "comm-2"] + } + mock_fetch_text.return_value = related_data + mock_fetch_commentaries.return_value = [ + {"id": "comm-1", "title": {"en": "Commentary 1"}, "language": "bo"}, + {"id": "comm-2", "title": {"en": "Commentary 2"}, "language": "bo"} + ] + + result = await _fetch_commentaries_from_related("trans-1", 0, 10) + + assert len(result) == 2 + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + async def test_fetch_commentaries_from_related_not_found(self, mock_fetch_text): + mock_fetch_text.return_value = None + + result = await _fetch_commentaries_from_related("missing", 0, 10) + + assert len(result) == 0 + + @pytest.mark.asyncio + @patch('pecha_api.texts.texts_openpecha_service.fetch_text_by_id') + async def test_fetch_commentaries_from_related_error(self, mock_fetch_text): + mock_fetch_text.side_effect = Exception("Connection error") + + result = await _fetch_commentaries_from_related("trans-1", 0, 10) + + assert len(result) == 0 + class TestFetchCommentaryDetails: """Tests for fetch_commentary_details function.""" diff --git a/tests/texts/test_texts_openpecha_services.py b/tests/texts/test_texts_openpecha_services.py index a28296dbc..64e3f7c94 100644 --- a/tests/texts/test_texts_openpecha_services.py +++ b/tests/texts/test_texts_openpecha_services.py @@ -6,6 +6,7 @@ from pecha_api.texts.texts_openpecha_service import get_text_detail_by_id, trim_segment_content from pecha_api.texts.text_openpecha_response_models import ( TextDetailResponse, + TextDetailWithContentResponse, CriticalEditionModel, ContributionModel, SegmentationResponseModel, @@ -87,12 +88,13 @@ async def test_get_text_detail_by_id_success(mocker): result = await get_text_detail_by_id(text_id=TEXT_ID, offset=0, limit=30) - assert result.id == TEXT_ID - assert result.edition_details == MOCK_EDITIONS - assert len(result.segments.contents) == 2 - assert result.segments.has_more is False - assert result.segments.offset == 0 - assert result.segments.limit == 30 + assert isinstance(result, TextDetailWithContentResponse) + assert result.text_detail.id == TEXT_ID + assert result.text_detail.title == "Test Text" + assert result.size == 2 + assert result.current_segment_position == 1 + assert len(result.content.sections) == 1 + assert len(result.content.sections[0].segments) == 2 @pytest.mark.asyncio diff --git a/tests/texts/test_texts_openpecha_views.py b/tests/texts/test_texts_openpecha_views.py index 366679bb3..a955a231b 100644 --- a/tests/texts/test_texts_openpecha_views.py +++ b/tests/texts/test_texts_openpecha_views.py @@ -79,6 +79,29 @@ # ============================================================================= class TestTextsV2Endpoint: + @patch("pecha_api.texts.texts_openpecha_views.get_texts_by_collection_from_openpecha") + def test_get_texts_without_collection_id(self, mock_service): + mock_service.return_value = V2TextsCategoryResponse( + collection=None, + texts=[V2TextDTO(id="t1", title="Text 1", language="en")], + skip=0, + limit=10, + ) + + response = client.get("/texts") + + assert response.status_code == 200 + data = response.json() + assert data["collection"] is None + assert len(data["texts"]) == 1 + mock_service.assert_awaited_once_with( + collection_id=None, + language=None, + title=None, + skip=0, + limit=10, + ) + @patch("pecha_api.texts.texts_openpecha_views.get_texts_by_collection_from_openpecha") def test_get_texts_by_collection_success(self, mock_service): mock_service.return_value = V2TextsCategoryResponse( @@ -91,13 +114,33 @@ def test_get_texts_by_collection_success(self, mock_service): limit=10, ) - response = client.get("/v2/texts/collection/cat-1") + response = client.get("/texts?collection_id=cat-1") assert response.status_code == 200 data = response.json() assert data["collection"]["title"] == "Discourses" assert len(data["texts"]) == 2 + @patch("pecha_api.texts.texts_openpecha_views.get_texts_by_collection_from_openpecha") + def test_get_texts_by_collection_with_title_filter(self, mock_service): + mock_service.return_value = V2TextsCategoryResponse( + collection=V2CollectionModel(id="cat-1", title="Discourses"), + texts=[V2TextDTO(id="t1", title="Heart Sutra", language="en")], + skip=0, + limit=10, + ) + + response = client.get("/texts?collection_id=cat-1&title=heart&skip=0&limit=10") + + assert response.status_code == 200 + mock_service.assert_awaited_once_with( + collection_id="cat-1", + language=None, + title="heart", + skip=0, + limit=10, + ) + @patch("pecha_api.texts.texts_openpecha_views.get_text_by_id_from_openpecha") def test_get_text_by_id_success(self, mock_service): mock_service.return_value = V2TextDTO( @@ -107,7 +150,7 @@ def test_get_text_by_id_success(self, mock_service): license="CC0", ) - response = client.get("/v2/texts/t1") + response = client.get("/texts/t1") assert response.status_code == 200 data = response.json() @@ -117,11 +160,11 @@ def test_get_text_by_id_success(self, mock_service): class TestTextsV2ValidationErrors: def test_invalid_skip_negative(self): - response = client.get("/v2/texts/collection/cat-1?skip=-1") + response = client.get("/texts?collection_id=cat-1&skip=-1") assert response.status_code == 422 def test_invalid_limit_zero(self): - response = client.get("/v2/texts/collection/cat-1?limit=0") + response = client.get("/texts?collection_id=cat-1&limit=0") assert response.status_code == 422 @@ -133,7 +176,7 @@ def test_get_texts_upstream_error(self, mock_service): detail="Failed to fetch texts from upstream service", ) - response = client.get("/v2/texts/collection/cat-1") + response = client.get("/texts?collection_id=cat-1") assert response.status_code == 502 assert "upstream" in response.json()["detail"].lower() @@ -145,7 +188,7 @@ def test_get_text_by_id_not_found(self, mock_service): detail="Text with id 'missing' not found", ) - response = client.get("/v2/texts/missing") + response = client.get("/texts/missing") assert response.status_code == 404 @@ -165,7 +208,7 @@ def test_get_text_versions_success(self, mock_service): ) mock_service.return_value = mock_response - response = client.get("/v2/texts/text-123/versions") + response = client.get("/texts/text-123/versions") assert response.status_code == 200 data = response.json() @@ -178,28 +221,6 @@ def test_get_text_versions_success(self, mock_service): assert data["versions"][1]["id"] == "version-2" mock_service.assert_called_once_with( text_id="text-123", - language=None, - skip=0, - limit=10 - ) - - @patch('pecha_api.texts.texts_openpecha_views.get_text_versions_from_openpecha') - def test_get_text_versions_with_language_filter(self, mock_service): - mock_response = TextVersionResponse( - text=MOCK_TEXT_DTO, - versions=[MOCK_TEXT_VERSION_1] - ) - mock_service.return_value = mock_response - - response = client.get("/v2/texts/text-123/versions?language=en") - - assert response.status_code == 200 - data = response.json() - assert len(data["versions"]) == 1 - assert data["versions"][0]["language"] == "en" - mock_service.assert_called_once_with( - text_id="text-123", - language="en", skip=0, limit=10 ) @@ -212,14 +233,13 @@ def test_get_text_versions_with_pagination(self, mock_service): ) mock_service.return_value = mock_response - response = client.get("/v2/texts/text-123/versions?skip=1&limit=5") + response = client.get("/texts/text-123/versions?skip=1&limit=5") assert response.status_code == 200 data = response.json() assert len(data["versions"]) == 1 mock_service.assert_called_once_with( text_id="text-123", - language=None, skip=1, limit=5 ) @@ -232,7 +252,7 @@ def test_get_text_versions_empty_versions(self, mock_service): ) mock_service.return_value = mock_response - response = client.get("/v2/texts/text-123/versions") + response = client.get("/texts/text-123/versions") assert response.status_code == 200 data = response.json() @@ -247,12 +267,11 @@ def test_get_text_versions_with_all_parameters(self, mock_service): ) mock_service.return_value = mock_response - response = client.get("/v2/texts/text-123/versions?language=bo&skip=2&limit=20") + response = client.get("/texts/text-123/versions?skip=2&limit=20") assert response.status_code == 200 mock_service.assert_called_once_with( text_id="text-123", - language="bo", skip=2, limit=20 ) @@ -268,7 +287,7 @@ def test_get_text_versions_not_found(self, mock_service): detail="Text with id 'nonexistent' not found" ) - response = client.get("/v2/texts/nonexistent/versions") + response = client.get("/texts/nonexistent/versions") assert response.status_code == 404 assert "not found" in response.json()["detail"].lower() @@ -280,7 +299,7 @@ def test_get_text_versions_upstream_error(self, mock_service): detail="Failed to fetch text from external API" ) - response = client.get("/v2/texts/text-123/versions") + response = client.get("/texts/text-123/versions") assert response.status_code == 502 assert "external" in response.json()["detail"].lower() or "failed" in response.json()["detail"].lower() @@ -292,7 +311,7 @@ def test_get_text_versions_internal_error(self, mock_service): detail="Internal server error" ) - response = client.get("/v2/texts/text-123/versions") + response = client.get("/texts/text-123/versions") assert response.status_code == 500 @@ -308,7 +327,7 @@ def test_response_contains_all_text_fields(self, mock_service): ) mock_service.return_value = mock_response - response = client.get("/v2/texts/text-123/versions") + response = client.get("/texts/text-123/versions") assert response.status_code == 200 text_data = response.json()["text"] @@ -328,7 +347,7 @@ def test_response_contains_all_version_fields(self, mock_service): ) mock_service.return_value = mock_response - response = client.get("/v2/texts/text-123/versions") + response = client.get("/texts/text-123/versions") assert response.status_code == 200 version_data = response.json()["versions"][0] @@ -365,7 +384,7 @@ def test_response_with_optional_fields_none(self, mock_service): ) mock_service.return_value = mock_response - response = client.get("/v2/texts/text-123/versions") + response = client.get("/texts/text-123/versions") assert response.status_code == 200 version_data = response.json()["versions"][0] @@ -405,7 +424,7 @@ def test_get_text_commentaries_success(self, mock_service): ) mock_service.return_value = [mock_commentary] - response = client.get("/v2/texts/text-123/commentaries") + response = client.get("/texts/text-123/commentaries") assert response.status_code == 200 data = response.json() @@ -443,7 +462,7 @@ def test_get_text_commentaries_multiple(self, mock_service): ] mock_service.return_value = mock_commentaries - response = client.get("/v2/texts/text-123/commentaries") + response = client.get("/texts/text-123/commentaries") assert response.status_code == 200 data = response.json() @@ -456,7 +475,7 @@ def test_get_text_commentaries_multiple(self, mock_service): def test_get_text_commentaries_empty(self, mock_service): mock_service.return_value = [] - response = client.get("/v2/texts/text-123/commentaries") + response = client.get("/texts/text-123/commentaries") assert response.status_code == 200 data = response.json() @@ -466,7 +485,7 @@ def test_get_text_commentaries_empty(self, mock_service): def test_get_text_commentaries_with_pagination(self, mock_service): mock_service.return_value = [] - response = client.get("/v2/texts/text-123/commentaries?skip=5&limit=20") + response = client.get("/texts/text-123/commentaries?skip=5&limit=20") assert response.status_code == 200 mock_service.assert_called_once_with( @@ -479,7 +498,7 @@ def test_get_text_commentaries_with_pagination(self, mock_service): def test_get_text_commentaries_with_skip_only(self, mock_service): mock_service.return_value = [] - response = client.get("/v2/texts/text-123/commentaries?skip=10") + response = client.get("/texts/text-123/commentaries?skip=10") assert response.status_code == 200 mock_service.assert_called_once_with( @@ -492,7 +511,7 @@ def test_get_text_commentaries_with_skip_only(self, mock_service): def test_get_text_commentaries_with_limit_only(self, mock_service): mock_service.return_value = [] - response = client.get("/v2/texts/text-123/commentaries?limit=50") + response = client.get("/texts/text-123/commentaries?limit=50") assert response.status_code == 200 mock_service.assert_called_once_with( @@ -502,11 +521,11 @@ def test_get_text_commentaries_with_limit_only(self, mock_service): ) def test_get_text_commentaries_invalid_skip(self): - response = client.get("/v2/texts/text-123/commentaries?skip=-1") + response = client.get("/texts/text-123/commentaries?skip=-1") assert response.status_code == 422 def test_get_text_commentaries_invalid_limit(self): - response = client.get("/v2/texts/text-123/commentaries?limit=101") + response = client.get("/texts/text-123/commentaries?limit=101") assert response.status_code == 422 @@ -520,7 +539,7 @@ def test_get_text_commentaries_not_found(self, mock_service): detail="Text with id 'nonexistent' not found" ) - response = client.get("/v2/texts/nonexistent/commentaries") + response = client.get("/texts/nonexistent/commentaries") assert response.status_code == 404 assert "not found" in response.json()["detail"].lower() @@ -532,7 +551,7 @@ def test_get_text_commentaries_upstream_error(self, mock_service): detail="Failed to fetch commentaries from external API" ) - response = client.get("/v2/texts/text-123/commentaries") + response = client.get("/texts/text-123/commentaries") assert response.status_code == 502 @@ -543,7 +562,7 @@ def test_get_text_commentaries_internal_error(self, mock_service): detail="Internal server error" ) - response = client.get("/v2/texts/text-123/commentaries") + response = client.get("/texts/text-123/commentaries") assert response.status_code == 500 @@ -573,7 +592,7 @@ def test_commentary_contains_all_required_fields(self, mock_service): ) mock_service.return_value = [mock_commentary] - response = client.get("/v2/texts/text-123/commentaries") + response = client.get("/texts/text-123/commentaries") assert response.status_code == 200 data = response.json()[0] @@ -616,7 +635,7 @@ def test_commentary_with_optional_fields_none(self, mock_service): ) mock_service.return_value = [mock_commentary] - response = client.get("/v2/texts/text-123/commentaries") + response = client.get("/texts/text-123/commentaries") assert response.status_code == 200 data = response.json()[0] diff --git a/tests/texts/test_texts_service.py b/tests/texts/test_texts_service.py index 643d17a42..c491c665f 100644 --- a/tests/texts/test_texts_service.py +++ b/tests/texts/test_texts_service.py @@ -1,383 +1,27 @@ -from unittest.mock import AsyncMock, patch, MagicMock, Mock -import httpx +from unittest.mock import AsyncMock, patch from fastapi import HTTPException from uuid import uuid4 -from pecha_api.collections.collections_response_models import CollectionModel import pytest from pecha_api.texts.texts_service import ( - create_new_text, - get_text_versions_by_group_id, - get_text_by_text_id_or_collection, create_table_of_content, - get_table_of_contents_by_text_id, - get_text_details_by_text_id, update_text_details, - remove_table_of_content_by_text_id, - delete_text_by_text_id, - get_sheet, - get_table_of_content_by_sheet_id, get_table_of_content_by_type, - _validate_text_detail_request, get_root_text_by_collection_id, - get_commentaries_by_text_id, replace_pecha_segment_id_with_segment_id, - get_text_by_pecha_text_ids_service, - get_titles_and_ids_by_query, - get_text_languages, - get_language_versions, - get_version_info ) -from pecha_api.terms.terms_response_models import TermsModel from pecha_api.texts.texts_response_models import ( - CreateTextRequest, TextDTO, - TextVersion, TableOfContent, TableOfContentType, Section, TextSegment, - TableOfContentResponse, - TextDetailsRequest, - DetailTableOfContentResponse, - DetailTableOfContent, - DetailSection, - DetailTextSegment, - Translation, UpdateTextRequest, - TextDTOResponse, - TextVersionResponse, - TextsCategoryResponse, TextsByPechaTextIdsRequest, - LanguageResponse, - AvailableLanguage, - VersionDetail, - VersionsResponse ) from pecha_api.recitations.recitations_response_models import RecitationDTO, RecitationsResponse -from pecha_api.texts.texts_enums import TextType, PaginationDirection, LANGUAGE_ORDERS -from pecha_api.sheets.sheets_enum import SortBy, SortOrder - from pecha_api.error_contants import ErrorConstants -from typing import List - -@pytest.mark.asyncio -async def test_get_text_by_text_id_or_collection_without_collection_id_success(): - text_id = "efb26a06-f373-450b-ba57-e7a8d4dd5b64" - collection_id = None - with patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock) as mock_get_text_detail_by_id, \ - patch("pecha_api.texts.texts_service.set_text_by_text_id_or_collection_cache", new_callable=AsyncMock, return_value=None), \ - patch("pecha_api.texts.texts_service.get_text_by_text_id_or_collection_cache", new_callable=AsyncMock, return_value=None): - mock_get_text_detail_by_id.return_value = TextDTO( - id=text_id, - title="བྱང་ཆུབ་སེམས་དཔའི་སྤྱོད་པ་ལ་འཇུག་པ།", - language="bo", - group_id="group_id_1", - type="commentary", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0 - ) - - response = await get_text_by_text_id_or_collection(text_id=text_id, collection_id=collection_id) - - assert response is not None - assert isinstance(response, TextDTO) - assert response.id == text_id - -@pytest.mark.asyncio -async def test_get_text_by_collection_id(): - mock_collection = CollectionModel( - id="id_1", - title="སྤྱོད་འཇུག", - description="དུས་རབས་ ༨ པའི་ནང་སློབ་དཔོན་ཞི་བ་ལྷས་མཛད་པའི་རྩ་བ་དང་དེའི་འགྲེལ་བ་སོགས།", - language="bo", - slug="bodhicaryavatara", - has_child=False - ) - - # Use valid UUID for group_id - valid_group_id = "123e4567-e89b-12d3-a456-426614174001" - - mock_texts_by_category = [ - TextDTO( - id="a48c0814-ce56-4ada-af31-f74b179b52a9", - title="སྤྱོད་འཇུག་དཀའ་འགྲེལ།", - language="bo", - group_id=valid_group_id, - type="commentary", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0, - pecha_text_id="pecha_1" - ), - TextDTO( - id="032b9a5f-0712-40d8-b7ec-73c8c94f1c15", - title="བྱང་ཆུབ་སེམས་དཔའི་སྤྱོད་པ་ལ་འཇུག་པ།", - language="bo", - group_id=valid_group_id, - type="version", - is_published=True, - created_date="2025-03-20 09:26:16.571522", - updated_date="2025-03-20 09:26:16.571532", - published_date="2025-03-20 09:26:16.571536", - published_by="pecha", - categories=[], - views=0, - pecha_text_id="pecha_2" - ) - ] - - with patch('pecha_api.texts.texts_service.get_collection', new_callable=AsyncMock, return_value=mock_collection), \ - patch('pecha_api.texts.texts_service.get_texts_by_collection', new_callable=AsyncMock) as mock_get_texts_by_category, \ - patch('pecha_api.texts.texts_service.get_all_texts_by_collection', new_callable=AsyncMock) as mock_get_all_texts, \ - patch('pecha_api.texts.texts_service.set_text_by_text_id_or_collection_cache', new_callable=AsyncMock, return_value=None), \ - patch('pecha_api.texts.texts_service.TextUtils.filter_text_base_on_group_id_type_and_language_preference', new_callable=AsyncMock) as mock_filter_text_base_on_group_id_type: - mock_filter_text_base_on_group_id_type.return_value = {"root_text": mock_texts_by_category[1], "commentary": [mock_texts_by_category[0]]} - mock_get_texts_by_category.return_value = mock_texts_by_category - # Return only the root text for total count calculation - mock_get_all_texts.return_value = [mock_texts_by_category[1]] - response = await get_text_by_text_id_or_collection(text_id=None, collection_id="id_1", language="bo", skip=0, limit=10) - assert response is not None - assert response.collection is not None - collection: CollectionModel = response.collection - assert collection.id == "id_1" - assert collection.slug == "bodhicaryavatara" - assert response.texts is not None - texts: List[TextDTO] = response.texts - assert len(texts) == 1 - assert texts[0] is not None - assert isinstance(texts[0], TextDTO) - assert texts[0].id == mock_texts_by_category[1].id - assert texts[0].title == mock_texts_by_category[1].title - assert texts[0].language == mock_texts_by_category[1].language - assert texts[0].type == "root_text" - assert response.total == 1 - assert response.skip == 0 - assert response.limit == 10 - - -@pytest.mark.asyncio -async def test_get_versions_by_group_id(): - text_detail = TextDTO( - id="id_1", - title="བྱང་ཆུབ་སེམས་དཔའི་སྤྱོད་པ་ལ་འཇུག་པ།", - group_id="group_id_1", - language="bo", - type="version", - is_published=True, - created_date="2025-03-20 09:26:16.571522", - updated_date="2025-03-20 09:26:16.571532", - published_date="2025-03-20 09:26:16.571536", - published_by="pecha", - categories=[], - views=0 - ) - texts_by_group_id = [ - TextDTO( - id="text_id_1", - title="བྱང་ཆུབ་སེམས་དཔའི་སྤྱོད་པ་ལ་འཇུག་པ།", - group_id="group_id_1", - language="bo", - type="version", - is_published=True, - created_date="2025-03-20 09:26:16.571522", - updated_date="2025-03-20 09:26:16.571532", - published_date="2025-03-20 09:26:16.571536", - published_by="pecha", - categories=[], - views=0 - ), - TextDTO( - id="text_id_2", - title="The Way of the Bodhisattva", - language="en", - group_id="group_id_1", - type="version", - is_published=True, - created_date="2025-03-20 09:28:28.076920", - updated_date="2025-03-20 09:28:28.076934", - published_date="2025-03-20 09:28:28.076938", - published_by="pecha", - categories=[], - views=0 - ), - TextDTO( - id="text_id_3", - title="शबोधिचर्यावतार", - language="sa", - group_id="group_id_1", - type="version", - is_published=True, - created_date="2025-03-20 09:29:51.154697", - updated_date="2025-03-20 09:29:51.154708", - published_date="2025-03-20 09:29:51.154712", - published_by="pecha", - categories=[], - views=0 - ) - ] - mock_table_of_content = TableOfContent( - id="table_of_content_id", - text_id="text_id_1", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="id_1", - title="section_1", - section_number=1, - parent_id="id_1", - segments=[], - sections=[], - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652" - ) - ] - ) - language = "en" - with patch('pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id', new_callable=AsyncMock) as mock_text_detail, \ - patch("pecha_api.texts.texts_service.get_text_versions_by_group_id_cache", new_callable=AsyncMock, return_value=None),\ - patch("pecha_api.texts.texts_service.set_text_versions_by_group_id_cache", new_callable=AsyncMock, return_value=None),\ - patch('pecha_api.texts.texts_service.get_texts_by_group_id', new_callable=AsyncMock) as mock_get_texts_by_group_id,\ - patch('pecha_api.texts.texts_service.get_contents_by_id', new_callable=AsyncMock) as mock_get_contents_by_id: - mock_text_detail.return_value = text_detail - mock_get_texts_by_group_id.return_value = texts_by_group_id - mock_get_contents_by_id.return_value = [mock_table_of_content] - response = await get_text_versions_by_group_id(text_id="id_1",language=language, skip=0, limit=10) - assert response is not None - assert response.text is not None - assert isinstance(response.text, TextDTO) - assert response.text.type == "version" - assert response.text.language == language - assert response.text.id == "text_id_2" - assert response.versions is not None - assert len(response.versions) == 2 - assert response.versions[0] is not None - assert isinstance(response.versions[0], TextVersion) - assert response.versions[0].id == "text_id_1" - for version in response.versions: - assert isinstance(version, TextVersion) - assert version.type == "version" - - - -@pytest.mark.asyncio -async def test_create_new_text(): - text_id = "efb26a06-f373-450b-ba57-e7a8d4dd5b64" - title = "བྱང་ཆུབ་སེམས་དཔའི་སྤྱོད་པ་ལ་འཇུག་པ།" - language = "bo" - is_published = True - group_id = "67dd22a8d9f06ab28feedc90" - created_date = "2025-03-16 04:40:54.757652" - updated_date = "2025-03-16 04:40:54.757652" - published_date = "2025-03-16 04:40:54.757652" - published_by = "pecha" - type_ = TextType.VERSION - categories = [] - with patch('pecha_api.texts.texts_service.validate_user_exists', return_value=True), \ - patch('pecha_api.texts.texts_service.create_text', new_callable=AsyncMock) as mock_create_text,\ - patch('pecha_api.texts.texts_service.validate_group_exists', new_callable=AsyncMock) as mock_validate_group_exists: - mock_create_text.return_value = AsyncMock( - id=text_id, - pecha_text_id="test_pecha_id", - title=title, - language=language, - is_published=is_published, - group_id=group_id, - created_date=created_date, - updated_date=updated_date, - published_date=published_date, - published_by=published_by, - type=type_, - categories=categories, - views=0, - source_link="https://test-source.com", - ranking=1, - license="CC0" - ) - mock_validate_group_exists.return_value = True - response = await create_new_text( - create_text_request=CreateTextRequest( - pecha_text_id="test_pecha_id", - title=title, - language=language, - group_id=group_id, - published_by=published_by, - type=type_, - categories=categories, - source_link="https://test-source.com", - ranking=1, - license="CC0" - ), - token="admin" - ) - assert response is not None - assert isinstance(response, TextDTO) - assert response.id == text_id - assert response.title == title - assert response.language == language - assert response.type == type_.value - assert response.is_published == is_published - assert response.created_date == created_date - assert response.updated_date == updated_date - assert response.published_date == published_date - assert response.published_by == published_by - assert response.categories == categories - -@pytest.mark.asyncio -async def test_create_new_text_invalid_group_id(): - with patch("pecha_api.texts.texts_service.validate_user_exists", return_value=True), \ - patch("pecha_api.texts.texts_service.validate_group_exists", return_value=False): - with pytest.raises(HTTPException) as exc_info: - await create_new_text( - create_text_request=CreateTextRequest( - pecha_text_id="test_pecha_id", - title="བྱང་ཆུབ་སེམས་དཔའི་སྤྱོད་པ་ལ་འཇུག་པ།", - language="bo", - group_id="invalid_group_id", - published_by="pecha", - type=TextType.VERSION, - categories=[], - source_link="https://test-source.com", - ranking=1, - license="CC0" - ), - token="user" - ) - assert exc_info.value.status_code == 404 - assert exc_info.value.detail == ErrorConstants.GROUP_NOT_FOUND_MESSAGE - -@pytest.mark.asyncio -async def test_create_new_text_invalid_user(): - with patch("pecha_api.texts.texts_service.validate_user_exists", return_value=False): - with pytest.raises(HTTPException) as exc_info: - await create_new_text( - create_text_request=CreateTextRequest( - pecha_text_id="test_pecha_id", - title="བྱང་ཆུབ་སེམས་དཔའི་སྤྱོད་པ་ལ་འཇུག་པ།", - language="bo", - group_id="67dd22a8d9f06ab28feedc90", - published_by="pecha", - type=TextType.VERSION, - categories=[], - source_link="https://test-source.com", - ranking=1, - license="CC0" - ), - token="user" - ) - assert exc_info.value.status_code == 401 - assert exc_info.value.detail == ErrorConstants.TOKEN_ERROR_MESSAGE @pytest.mark.asyncio async def test_create_table_of_content_success(): @@ -496,311 +140,6 @@ async def test_create_table_of_content_invalid_segment(): assert ErrorConstants.SEGMENT_NOT_FOUND_MESSAGE in exc_info.value.detail assert str(segment_ids) in exc_info.value.detail -@pytest.mark.asyncio -async def test_get_table_of_contents_by_text_id_success(): - text_id = "text_id_1" - language = "bo" - skip = 0 - limit = 10 - - mock_text_detail = TextDTO( - id=text_id, - title="text_1", - language=language, - group_id="group_id_1", - type="version", - is_published=False, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="pecha", - categories=[], - views=0 - ) - mock_group_texts = [ - TextDTO( - id="text_id_1", - title="text_1", - language="bo", - group_id="group_id_1", - type="version", - is_published=False, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="pecha", - categories=[], - views=0 - ), - TextDTO( - id="text_id_2", - title="text_2", - language="en", - group_id="group_id_1", - type="version", - is_published=False, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="pecha", - categories=[], - views=0 - ) - ] - - table_of_contents = [ - TableOfContent( - id="id_1", - text_id=text_id, - type=TableOfContentType.TEXT, - sections=[ - Section( - id="id_1", - title="section_1", - section_number=1, - parent_id="id_1", - segments=[ - TextSegment( - segment_id="seg_1", - segment_number=1 - ) - ], - sections=[], - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652" - ) - ] - ) - ] - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.get_table_of_contents_by_text_id_cache", new_callable=MagicMock, return_value=None),\ - patch("pecha_api.texts.texts_service.set_table_of_contents_by_text_id_cache", new_callable=MagicMock, return_value=None),\ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_texts_by_group_id", new_callable=AsyncMock, return_value=mock_group_texts), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=table_of_contents): - - response = await get_table_of_contents_by_text_id( - text_id=text_id, - language=language, - skip=skip, - limit=limit - ) - - assert response is not None - assert isinstance(response, TableOfContentResponse) - assert response.text_detail is not None - assert isinstance(response.text_detail, TextDTO) - assert response.text_detail.id == mock_text_detail.id - assert response.text_detail.language == language - assert response.contents is not None - assert len(response.contents) == 1 - assert response.contents[0] is not None - assert isinstance(response.contents[0], TableOfContent) - assert response.contents[0].id == table_of_contents[0].id - assert response.contents[0].text_id == table_of_contents[0].text_id - assert response.contents[0].sections is not None - -@pytest.mark.asyncio -async def test_get_table_of_contents_by_text_id_success_language_is_none(): - text_id = "text_id_1" - language = "en" - skip = 0 - limit = 10 - - mock_text_detail = TextDTO( - id=text_id, - title="text_1", - language=language, - group_id="group_id_1", - type="version", - is_published=False, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="pecha", - categories=[], - views=0 - ) - mock_group_texts = [ - TextDTO( - id="text_id_1", - title="text_1", - language="bo", - group_id="group_id_1", - type="version", - is_published=False, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="pecha", - categories=[], - views=0 - ), - TextDTO( - id="text_id_2", - title="text_2", - language="en", - group_id="group_id_1", - type="version", - is_published=False, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="pecha", - categories=[], - views=0 - ) - ] - - table_of_contents = [ - TableOfContent( - id="id_1", - text_id=text_id, - type=TableOfContentType.TEXT, - sections=[ - Section( - id="id_1", - title="section_1", - section_number=1, - parent_id="id_1", - segments=[ - TextSegment( - segment_id="seg_1", - segment_number=1 - ) - ], - sections=[], - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652" - ) - ] - ) - ] - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.get_table_of_contents_by_text_id_cache", new_callable=MagicMock, return_value=None),\ - patch("pecha_api.texts.texts_service.set_table_of_contents_by_text_id_cache", new_callable=MagicMock, return_value=None),\ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_texts_by_group_id", new_callable=AsyncMock, return_value=mock_group_texts), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=table_of_contents): - - response = await get_table_of_contents_by_text_id( - text_id=text_id, - language=None, - skip=skip, - limit=limit - ) - - assert response is not None - assert isinstance(response, TableOfContentResponse) - assert response.text_detail is not None - assert isinstance(response.text_detail, TextDTO) - assert response.text_detail.language == language - assert response.contents is not None - assert len(response.contents) == 1 - assert response.contents[0] is not None - assert isinstance(response.contents[0], TableOfContent) - -@pytest.mark.asyncio -async def test_get_table_of_contents_by_text_id_root_text_is_none(): - """Test that HTTPException is raised when no root text can be determined after filtering""" - from pecha_api.constants import Constants - - text_id = "text_id_1" - language = "zh" # Requesting Chinese but no Chinese text exists - skip = 0 - limit = 10 - - mock_text_detail = TextDTO( - id=text_id, - title="text_1", - language="bo", - group_id="group_id_1", - type="version", - is_published=False, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="pecha", - categories=[], - views=0 - ) - - # Mock texts that are all in excluded_text_ids so root_text becomes None - excluded_id = "excluded_text_id_test" - mock_group_texts = [ - TextDTO( - id=excluded_id, - title="text_1", - language="bo", - group_id="group_id_1", - type="version", - is_published=False, - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652", - published_by="pecha", - categories=[], - views=0 - ) - ] - - table_of_contents = [ - TableOfContent( - id="id_1", - text_id=text_id, - type=TableOfContentType.TEXT, - sections=[ - Section( - id="id_1", - title="section_1", - section_number=1, - parent_id="id_1", - segments=[], - sections=[], - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652" - ) - ] - ) - ] - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.get_table_of_contents_by_text_id_cache", new_callable=MagicMock, return_value=None),\ - patch("pecha_api.texts.texts_service.set_table_of_contents_by_text_id_cache", new_callable=MagicMock, return_value=None),\ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_texts_by_group_id", new_callable=AsyncMock, return_value=mock_group_texts), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=table_of_contents), \ - patch("pecha_api.constants.Constants.excluded_text_ids", [excluded_id]): - - with pytest.raises(HTTPException) as exc_info: - await get_table_of_contents_by_text_id( - text_id=text_id, - language=language, - skip=skip, - limit=limit - ) - assert exc_info.value.status_code == 404 - assert exc_info.value.detail == ErrorConstants.TEXT_NOT_FOUND_MESSAGE - -@pytest.mark.asyncio -async def test_get_table_of_contents_by_text_id_invalid_text(): - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=False): - with pytest.raises(HTTPException) as exc_info: - await get_table_of_contents_by_text_id( - text_id="id_1", - language="en", - skip=0, - limit=10 - ) - assert exc_info.value.status_code == 404 - assert exc_info.value.detail == ErrorConstants.TEXT_NOT_FOUND_MESSAGE - - @pytest.mark.asyncio async def test_update_text_details_success(): mock_text_details = TextDTO( @@ -839,700 +178,20 @@ async def test_update_text_details_invalid_text_id(): assert exec_info.value.detail == ErrorConstants.TEXT_NOT_FOUND_MESSAGE @pytest.mark.asyncio -async def test_delete_table_of_content_success(): - with patch("pecha_api.texts.texts_service.delete_table_of_content_by_text_id", new_callable=AsyncMock), \ - patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True): - response = await remove_table_of_content_by_text_id(text_id="text_id_1") - assert response is not None - -@pytest.mark.asyncio -async def test_delete_table_of_content_invalid_text_id(): - with patch("pecha_api.texts.texts_service.delete_table_of_content_by_text_id", new_callable=AsyncMock), \ - patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=False): - with pytest.raises(HTTPException) as exec_info: - await remove_table_of_content_by_text_id(text_id="invalid_id") - assert exec_info.value.status_code == 404 - assert exec_info.value.detail == ErrorConstants.TEXT_NOT_FOUND_MESSAGE - -@pytest.mark.asyncio -async def test_delete_text_by_text_id_success(): - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.delete_text_by_id", new_callable=AsyncMock): - response = await delete_text_by_text_id(text_id="text_id_1") - assert response is None - -@pytest.mark.asyncio -async def test_delete_text_by_text_id_invalid_text_id(): - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=False): - with pytest.raises(HTTPException) as exc_info: - await delete_text_by_text_id(text_id="invalid_text_id") - - assert exc_info.value.status_code == 404 - assert exc_info.value.detail == ErrorConstants.TEXT_NOT_FOUND_MESSAGE - -@pytest.mark.asyncio -async def test_get_sheet_success_user_viewing_own_sheets(): - email = "test_user@gmail.com" - mock_sheets = [ - type("Text", (), { - "id": f"sheet_id_{i}", - "title": "Test Sheet", - "language": "en", - "group_id": "group_id", - "type": "sheet", - "is_published": True if i % 2 == 0 else False, - "created_date": "2021-01-01", - "updated_date": "2021-01-01", - "published_date": "2021-01-01", - "published_by": email, - "categories": [], - "views": 10 - })() - for i in range(1,11) - ] - with patch("pecha_api.texts.texts_service.fetch_sheets_from_db", new_callable=AsyncMock, return_value=mock_sheets): - - response = await get_sheet(published_by=email, is_published=None, sort_by=None, sort_order=None, skip=0, limit=10) - - assert response is not None - assert len(response) == 10 - for sheet in response: - assert sheet.published_by == email - -@pytest.mark.asyncio -async def test_get_text_details_by_text_id_with_text_id_content_id_segment_id_success(): - text_id = "text_id_1" - content_id = "content_id_1" - segment_id = "segment_id_1" - mock_text_detail = TextDTO( - id=text_id, - title="text_title", - language="bo", - group_id="group_id_1", - type="version", - is_published=False, - created_date="created_date", - updated_date="updated_date", - published_date="published_date", - published_by="published_by", - categories=[], - views=0 - ) - mock_table_of_content = TableOfContent( - id=content_id, - text_id=text_id, - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_id_1", - title="section_title", - section_number=1, - parent_id="parent_id_1", - segments=[ - TextSegment( - segment_id="segment_id_1", - segment_number=1 - ), - TextSegment( - segment_id="segment_id_2", - segment_number=2 - ), - TextSegment( - segment_id="segment_id_3", - segment_number=3 - ) - ], - sections=[], - created_date="created_date", - updated_date="updated_date", - published_date="published_date" - ) - ] - ) - mock_mapped_table_of_content = DetailTableOfContent( - id=content_id, - text_id=text_id, - sections=[ - DetailSection( - id="section_id_1", - title="section_title", - section_number=1, - parent_id="parent_id_1", - segments=[ - DetailTextSegment( - segment_id="segment_id_1", - segment_number=1, - content="segment_content_1", - translation=None - ), - DetailTextSegment( - segment_id="segment_id_2", - segment_number=2, - content="segment_content_2", - translation=None - ) - ], - sections=[], - created_date="created_date", - updated_date="updated_date", - published_date="published_date" - ) - ] - ) - - with patch("pecha_api.texts.texts_service._validate_text_detail_request", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_table_of_content_by_content_id", new_callable=AsyncMock, return_value=mock_table_of_content), \ - patch("pecha_api.texts.texts_service.SegmentUtils.get_mapped_segment_content_for_table_of_content", new_callable=AsyncMock, return_value=mock_mapped_table_of_content): - - response = await get_text_details_by_text_id( - text_id=text_id, - text_details_request=TextDetailsRequest( - content_id=content_id, - segment_id=segment_id, - size=2, - direction=PaginationDirection.NEXT - ) - ) - - assert response is not None - assert response.text_detail is not None - assert isinstance(response.text_detail, TextDTO) - assert response.text_detail.id == mock_text_detail.id - assert response.content is not None - assert isinstance(response.content, DetailTableOfContent) - assert response.content.sections is not None - assert len(response.content.sections) == 1 - assert response.content.sections[0] is not None - assert isinstance(response.content.sections[0], DetailSection) - section = response.content.sections[0] - assert section.segments is not None - assert len(section.segments) == 2 - assert section.segments[0].segment_id == "segment_id_1" - assert section.segments[1].segment_id == "segment_id_2" - assert response.pagination_direction == PaginationDirection.NEXT - -@pytest.mark.asyncio -async def test_get_text_details_by_text_id_with_text_id_content_id_segment_id_previous_success(): - text_id = "text_id_1" - content_id = "content_id_1" - segment_id = "segment_id_1" - mock_text_detail = TextDTO( - id=text_id, - title="text_title", - language="bo", - group_id="group_id_1", - type="version", - is_published=False, - created_date="created_date", - updated_date="updated_date", - published_date="published_date", - published_by="published_by", - categories=[], - views=0 - ) - mock_table_of_content = TableOfContent( - id=content_id, - text_id=text_id, - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_id_1", - title="section_title", - section_number=1, - parent_id="parent_id_1", - segments=[ - TextSegment( - segment_id="segment_id_1", - segment_number=1 - ), - TextSegment( - segment_id="segment_id_2", - segment_number=2 - ), - TextSegment( - segment_id="segment_id_3", - segment_number=3 - ) - ], - sections=[], - created_date="created_date", - updated_date="updated_date", - published_date="published_date" - ) - ] - ) - mock_mapped_table_of_content = DetailTableOfContent( - id=content_id, - text_id=text_id, - sections=[ - DetailSection( - id="section_id_1", - title="section_title", - section_number=1, - parent_id="parent_id_1", - segments=[ - DetailTextSegment( - segment_id="segment_id_1", - segment_number=1, - content="segment_content_1", - translation=None - ) - ], - sections=[], - created_date="created_date", - updated_date="updated_date", - published_date="published_date" - ) - ] - ) - - with patch("pecha_api.texts.texts_service._validate_text_detail_request", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_table_of_content_by_content_id", new_callable=AsyncMock, return_value=mock_table_of_content), \ - patch("pecha_api.texts.texts_service.SegmentUtils.get_mapped_segment_content_for_table_of_content", new_callable=AsyncMock, return_value=mock_mapped_table_of_content): - - response = await get_text_details_by_text_id( - text_id=text_id, - text_details_request=TextDetailsRequest( - content_id=content_id, - segment_id=segment_id, - size=2, - direction=PaginationDirection.PREVIOUS - ) - ) - - assert response is not None - assert response.text_detail is not None - assert isinstance(response.text_detail, TextDTO) - assert response.text_detail.id == mock_text_detail.id - assert response.content is not None - assert isinstance(response.content, DetailTableOfContent) - assert response.content.sections is not None - assert len(response.content.sections) == 1 - assert response.content.sections[0] is not None - assert isinstance(response.content.sections[0], DetailSection) - section = response.content.sections[0] - assert section.segments is not None - assert len(section.segments) == 1 - assert section.segments[0].segment_id == "segment_id_1" - assert response.pagination_direction == PaginationDirection.PREVIOUS - - -@pytest.mark.asyncio -async def test_get_text_details_by_text_id_with_segment_id_only_success(): - text_id = "text_id_1" - segment_id = "segment_id_1" - mock_text_detail = TextDTO( - id=text_id, - title="text_title", - language="bo", - group_id="group_id_1", - type="version", - is_published=False, - created_date="created_date", - updated_date="updated_date", - published_date="published_date", - published_by="published_by", - categories=[], - views=0 - ) - mock_table_of_contents = [ - TableOfContent( - id=f"content_id_{i}", - text_id=text_id, - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_id_1", - title="section_title", - section_number=1, - parent_id="parent_id_1", - segments=[ - TextSegment( - segment_id=f"segment_id_{i}", - segment_number=1 - ) - ], - sections=[], - created_date="created_date", - updated_date="updated_date", - published_date="published_date" - ) - ] - ) - for i in range(1,11) - ] - mock_mapped_table_of_contents = DetailTableOfContent( - id="content_id_1", - text_id=text_id, - sections=[ - DetailSection( - id="section_id_1", - title="section_title", - section_number=1, - parent_id="parent_id_1", - segments=[ - DetailTextSegment( - segment_id=f"segment_id_1", - segment_number=1, - content="segment_content_1", - translation=None - ) - ], - sections=[], - created_date="created_date", - updated_date="updated_date", - published_date="published_date" - ) - ] - ) - - with patch("pecha_api.texts.texts_service._validate_text_detail_request", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=mock_table_of_contents), \ - patch("pecha_api.texts.texts_service.SegmentUtils.get_mapped_segment_content_for_table_of_content", new_callable=AsyncMock, return_value=mock_mapped_table_of_contents): - - response = await get_text_details_by_text_id( - text_id=text_id, - text_details_request=TextDetailsRequest( - segment_id=segment_id, - size=2, - direction=PaginationDirection.NEXT - ) - ) - - assert response is not None - assert response.text_detail is not None - assert isinstance(response.text_detail, TextDTO) - assert response.text_detail.id == mock_text_detail.id - assert response.content is not None - assert isinstance(response.content, DetailTableOfContent) - assert response.content.sections is not None - assert len(response.content.sections) == 1 - assert response.content.sections[0] is not None - assert isinstance(response.content.sections[0], DetailSection) - section = response.content.sections[0] - assert section.segments is not None - assert len(section.segments) == 1 - assert section.segments[0].segment_id == segment_id - assert response.pagination_direction == PaginationDirection.NEXT - - - -@pytest.mark.asyncio -async def test_get_text_details_by_text_id_with_content_id_only_success(): - text_id = "text_id_1" - content_id = "content_id_1" - mock_text_detail = TextDTO( - id=text_id, - title="text_title", - language="bo", - group_id="group_id_1", - type="version", - is_published=False, - created_date="created_date", - updated_date="updated_date", - published_date="published_date", - published_by="published_by", - categories=[], - views=0 - ) - mock_table_of_contents = [ - TableOfContent( - id=f"content_id_{i}", - text_id=text_id, - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_id_1", - title="section_title", - section_number=1, - parent_id="parent_id_1", - segments=[ - TextSegment( - segment_id=f"segment_id_{i}", - segment_number=1 - ) - ], - sections=[ - Section( - id="section_id_2", - title="section_title_2", - section_number=2, - parent_id="parent_id_2", - segments=[ - TextSegment( - segment_id=f"1_segment_id_{i}", - segment_number=1 - ) - ], - sections=[], - created_date="created_date", - updated_date="updated_date", - published_date="published_date" - ) - ], - created_date="created_date", - updated_date="updated_date", - published_date="published_date" - ) - ] - ) - for i in range(1,11) - ] - mock_mapped_table_of_contents = DetailTableOfContent( - id="content_id_1", - text_id=text_id, - sections=[ - DetailSection( - id="section_id_1", - title="section_title", - section_number=1, - parent_id="parent_id_1", - segments=[ - DetailTextSegment( - segment_id=f"segment_id_1", - segment_number=1, - content="segment_content_1", - translation=None - ) - ], - sections=[ - DetailSection( - id="section_id_2", - title="section_title_2", - section_number=2, - parent_id="parent_id_2", - segments=[ - DetailTextSegment( - segment_id=f"1_segment_id_1", - segment_number=1, - content="segment_content_1", - translation=None - ) - ], - sections=[], - created_date="created_date", - updated_date="updated_date", - published_date="published_date" - ) - ], - created_date="created_date", - updated_date="updated_date", - published_date="published_date" - ) - ] - ) - - with patch("pecha_api.texts.texts_service._validate_text_detail_request", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=mock_table_of_contents), \ - patch("pecha_api.texts.texts_service.SegmentUtils.get_mapped_segment_content_for_table_of_content", new_callable=AsyncMock, return_value=mock_mapped_table_of_contents): - - response = await get_text_details_by_text_id( - text_id=text_id, - text_details_request=TextDetailsRequest( - content_id=content_id, - size=2, - direction=PaginationDirection.NEXT - ) - ) - - assert response is not None - assert response.text_detail is not None - assert isinstance(response.text_detail, TextDTO) - assert response.text_detail.id == mock_text_detail.id - assert response.content is not None - assert isinstance(response.content, DetailTableOfContent) - assert response.content.id == content_id - assert response.content.sections is not None - assert len(response.content.sections) == 1 - assert response.content.sections[0] is not None - assert isinstance(response.content.sections[0], DetailSection) - section = response.content.sections[0] - assert section.segments is not None - assert section.sections is not None - assert len(section.segments) + len(section.sections[0].segments) == 2 - assert section.segments[0].segment_id == "segment_id_1" - assert response.pagination_direction == PaginationDirection.NEXT - - -@pytest.mark.asyncio -async def test_get_table_of_content_by_sheet_id_success(): - sheet_id = "sheet_id_1" - mock_table_of_contents = [ - TableOfContent( - id="content_id_1", - text_id=sheet_id, - type=TableOfContentType.SHEET, - sections=[ - Section( - id="section_id_1", - title="section_title", - section_number=1, - parent_id="parent_id_1", - segments=[ - TextSegment( - segment_id="segment_id_1", - segment_number=1 - ) - ], - sections=[], - created_date="created_date", - updated_date="updated_date", - published_date="published_date" - ) - ] - ) - ] - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=mock_table_of_contents), \ - patch("pecha_api.texts.texts_service.set_table_of_content_by_sheet_id_cache", new_callable=AsyncMock, return_value=None): - - response = await get_table_of_content_by_sheet_id(sheet_id=sheet_id) - - assert response is not None - assert isinstance(response, TableOfContent) - assert response.id == "content_id_1" - -@pytest.mark.asyncio -async def test_get_table_of_content_by_sheet_id_invalid_sheet_id(): - sheet_id = "invalid_sheet_id" - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=False): - with pytest.raises(HTTPException) as exc_info: - await get_table_of_content_by_sheet_id(sheet_id=sheet_id) - - assert exc_info.value.status_code == 404 - assert exc_info.value.detail == ErrorConstants.TEXT_NOT_FOUND_MESSAGE - - -@pytest.mark.asyncio -async def test_validate_text_detail_request_success(): - text_id = "text_id_1" - content_id = "content_id_1" - version_id = "version_id_1" - segment_id = "segment_id_1" - section_id = "section_id_1" - size = 20 - direction = PaginationDirection.NEXT - - text_details_request = TextDetailsRequest( - content_id=content_id, - version_id=version_id, - segment_id=segment_id, - section_id=section_id, - size=size, - direction=direction - ) - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.SegmentUtils.validate_segment_exists", new_callable=AsyncMock, return_value=True): - - response = await _validate_text_detail_request(text_id=text_id, text_details_request=text_details_request) - -@pytest.mark.asyncio -async def test_validate_text_detail_request_text_id_is_none(): - text_id = None - content_id = "content_id_1" - version_id = "version_id_1" - segment_id = "segment_id_1" - section_id = "section_id_1" - size = 20 - direction = PaginationDirection.NEXT - - text_details_request = TextDetailsRequest( - content_id=content_id, - version_id=version_id, - segment_id=segment_id, - section_id=section_id, - size=size, - direction=direction - ) - - with pytest.raises(HTTPException) as e: - await _validate_text_detail_request(text_id=text_id, text_details_request=text_details_request) - - assert e.value.status_code == 400 - assert e.value.detail == ErrorConstants.TEXT_OR_TERM_NOT_FOUND_MESSAGE - -@pytest.mark.asyncio -async def test_validate_text_detail_request_invalid_version_id(): - text_id = None - content_id = "content_id_1" - version_id = "invalid_version_id_1" - segment_id = "segment_id_1" - section_id = "section_id_1" - size = 20 - direction = PaginationDirection.NEXT - - text_details_request = TextDetailsRequest( - content_id=content_id, - version_id=version_id, - segment_id=segment_id, - section_id=section_id, - size=size, - direction=direction - ) - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=False): - with pytest.raises(HTTPException) as e: - await _validate_text_detail_request(text_id=text_id, text_details_request=text_details_request) - assert e.value.status_code == 400 - assert e.value.detail == ErrorConstants.TEXT_OR_TERM_NOT_FOUND_MESSAGE - -@pytest.mark.asyncio -async def test_validate_text_detail_request_invalid_segment_id(): - text_id = None - content_id = "content_id_1" - version_id = "version_id_1" - segment_id = "invalid_segment_id_1" - section_id = "section_id_1" - size = 20 - direction = PaginationDirection.NEXT - - text_details_request = TextDetailsRequest( - content_id=content_id, - version_id=version_id, - segment_id=segment_id, - section_id=section_id, - size=size, - direction=direction - ) +async def test_get_root_text_by_collection_id_success_with_root_text(): + """Test get_root_text_by_collection_id when root text is found""" + collection_id = str(uuid4()) + language = "bo" + text_id_1 = str(uuid4()) + text_id_2 = str(uuid4()) + group_id_1 = str(uuid4()) - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.SegmentUtils.validate_segment_exists", new_callable=AsyncMock, return_value=False): - - with pytest.raises(HTTPException) as e: - await _validate_text_detail_request(text_id=text_id, text_details_request=text_details_request) - - assert e.value.status_code == 400 - assert e.value.detail == ErrorConstants.TEXT_OR_TERM_NOT_FOUND_MESSAGE - - -@pytest.mark.asyncio -async def test_get_versions_by_group_id_language_is_none(): - text_detail = TextDTO( - id="id_1", - title="བྱང་ཆུབ་སེམས་དཔའི་སྤྱོད་པ་ལ་འཇུག་པ།", - group_id="group_id_1", - language="bo", - type="version", - is_published=True, - created_date="2025-03-20 09:26:16.571522", - updated_date="2025-03-20 09:26:16.571532", - published_date="2025-03-20 09:26:16.571536", - published_by="pecha", - categories=[], - views=0 - ) - texts_by_group_id = [ + mock_texts = [ TextDTO( - id="text_id_1", + id=text_id_1, title="བྱང་ཆུབ་སེམས་དཔའི་སྤྱོད་པ་ལ་འཇུག་པ།", - group_id="group_id_1", language="bo", + group_id=group_id_1, type="version", is_published=True, created_date="2025-03-20 09:26:16.571522", @@ -1543,10 +202,10 @@ async def test_get_versions_by_group_id_language_is_none(): views=0 ), TextDTO( - id="text_id_2", + id=text_id_2, title="The Way of the Bodhisattva", language="en", - group_id="group_id_1", + group_id=group_id_1, type="version", is_published=True, created_date="2025-03-20 09:28:28.076920", @@ -1555,216 +214,16 @@ async def test_get_versions_by_group_id_language_is_none(): published_by="pecha", categories=[], views=0 - ), - TextDTO( - id="text_id_3", - title="शबोधिचर्यावतार", - language="sa", - group_id="group_id_1", - type="version", - is_published=True, - created_date="2025-03-20 09:29:51.154697", - updated_date="2025-03-20 09:29:51.154708", - published_date="2025-03-20 09:29:51.154712", - published_by="pecha", - categories=[], - views=0 ) ] - mock_table_of_content = TableOfContent( - id="table_of_content_id", - text_id="text_id_1", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="id_1", - title="section_1", - section_number=1, - parent_id="id_1", - segments=[], - sections=[], - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652" - ) - ] - ) - language = "en" - with patch('pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id', new_callable=AsyncMock) as mock_text_detail, \ - patch("pecha_api.texts.texts_service.get_text_versions_by_group_id_cache", new_callable=AsyncMock, return_value=None),\ - patch("pecha_api.texts.texts_service.set_text_versions_by_group_id_cache", new_callable=AsyncMock, return_value=None),\ - patch('pecha_api.texts.texts_service.get_texts_by_group_id', new_callable=AsyncMock) as mock_get_texts_by_group_id,\ - patch('pecha_api.texts.texts_service.get_contents_by_id', new_callable=AsyncMock) as mock_get_contents_by_id: - mock_text_detail.return_value = text_detail - mock_get_texts_by_group_id.return_value = texts_by_group_id - mock_get_contents_by_id.return_value = [mock_table_of_content] - response = await get_text_versions_by_group_id(text_id="id_1",language=None, skip=0, limit=10) - assert response is not None - assert response.text is not None - assert isinstance(response.text, TextDTO) - assert response.text.type == "version" - assert response.text.language == language - assert response.text.id == "text_id_2" - assert response.versions is not None - assert len(response.versions) == 2 - assert response.versions[0] is not None - assert isinstance(response.versions[0], TextVersion) - assert response.versions[0].id == "text_id_1" - for version in response.versions: - assert isinstance(version, TextVersion) - assert version.type == "version" - -@pytest.mark.asyncio -async def test_get_versions_by_group_id_cache_data_is_not_none(): - """Test get_text_versions_by_group_id returns cached data when available""" - text_detail = TextDTO( - id="id_1", - title="བྱང་ཆུབ་སེམས་དཔའི་སྤྱོད་པ་ལ་འཇུག་པ།", - group_id="group_id_1", - language="bo", - type="version", - is_published=True, - created_date="2025-03-20 09:26:16.571522", - updated_date="2025-03-20 09:26:16.571532", - published_date="2025-03-20 09:26:16.571536", - published_by="pecha", - categories=[], - views=0 - ) - texts_by_group_id = [ - TextDTO( - id="text_id_1", - title="བྱང་ཆུབ་སེམས་དཔའི་སྤྱོད་པ་ལ་འཇུག་པ།", - group_id="group_id_1", - language="bo", - type="version", - is_published=True, - created_date="2025-03-20 09:26:16.571522", - updated_date="2025-03-20 09:26:16.571532", - published_date="2025-03-20 09:26:16.571536", - published_by="pecha", - categories=[], - views=0 - ), - TextDTO( - id="text_id_2", - title="The Way of the Bodhisattva", - language="en", - group_id="group_id_1", - type="version", - is_published=True, - created_date="2025-03-20 09:28:28.076920", - updated_date="2025-03-20 09:28:28.076934", - published_date="2025-03-20 09:28:28.076938", - published_by="pecha", - categories=[], - views=0 - ), - TextDTO( - id="text_id_3", - title="शबोधिचर्यावतार", - language="sa", - group_id="group_id_1", - type="version", - is_published=True, - created_date="2025-03-20 09:29:51.154697", - updated_date="2025-03-20 09:29:51.154708", - published_date="2025-03-20 09:29:51.154712", - published_by="pecha", - categories=[], - views=0 - ) - ] - cache_text_version = TextVersionResponse( - text=text_detail, - versions=[ - TextVersion( - id="text_id_1", - title="བྱང་ཆུབ་སེམས་དཔའི་སྤྱོད་པ་ལ་འཇུག་པ།", - parent_id=None, - priority=None, - language="bo", - type="version", - group_id="group_id_1", - table_of_contents=[], - is_published=True, - created_date="2025-03-20 09:26:16.571522", - updated_date="2025-03-20 09:26:16.571532", - published_date="2025-03-20 09:26:16.571536", - published_by="pecha" - ) - ] - ) - language = "en" - - # Mock TextUtils.get_text_detail_by_id which is called before cache check - # Note: The actual implementation first gets the root text, then checks cache - with patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=text_detail), \ - patch("pecha_api.texts.texts_service.get_texts_by_group_id", new_callable=AsyncMock, return_value=texts_by_group_id), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=[]), \ - patch("pecha_api.texts.texts_service.set_text_versions_by_group_id_cache", new_callable=AsyncMock): - - response = await get_text_versions_by_group_id(text_id="id_1", language=language, skip=0, limit=10) - - assert response is not None - assert isinstance(response, TextVersionResponse) - assert response.text is not None - assert isinstance(response.text, TextDTO) - assert response.versions is not None - # When language="en", filter_text_on_root_and_version selects text_id_2 (English) as root - assert response.text.id == "text_id_2" - assert response.text.language == "en" - # The other texts become versions - assert len(response.versions) == 2 - - -@pytest.mark.asyncio -async def test_get_root_text_by_collection_id_success_with_root_text(): - """Test get_root_text_by_collection_id when root text is found""" - collection_id = str(uuid4()) - language = "bo" - text_id_1 = str(uuid4()) - text_id_2 = str(uuid4()) - group_id_1 = str(uuid4()) - - mock_texts = [ - TextDTO( - id=text_id_1, - title="བྱང་ཆུབ་སེམས་དཔའི་སྤྱོད་པ་ལ་འཇུག་པ།", - language="bo", - group_id=group_id_1, - type="version", - is_published=True, - created_date="2025-03-20 09:26:16.571522", - updated_date="2025-03-20 09:26:16.571532", - published_date="2025-03-20 09:26:16.571536", - published_by="pecha", - categories=[], - views=0 - ), - TextDTO( - id=text_id_2, - title="The Way of the Bodhisattva", - language="en", - group_id=group_id_1, - type="version", - is_published=True, - created_date="2025-03-20 09:28:28.076920", - updated_date="2025-03-20 09:28:28.076934", - published_date="2025-03-20 09:28:28.076938", - published_by="pecha", - categories=[], - views=0 - ) - ] - - mock_filtered_result = { - "root_text": mock_texts[0], - "versions": [mock_texts[1]] - } - - with patch("pecha_api.texts.texts_service.get_all_recitation_texts_by_collection", new_callable=AsyncMock) as mock_get_all_texts, \ - patch("pecha_api.texts.texts_service.TextUtils.filter_text_base_on_group_id_type_and_language_preference", new_callable=AsyncMock) as mock_filter: + + mock_filtered_result = { + "root_text": mock_texts[0], + "versions": [mock_texts[1]] + } + + with patch("pecha_api.texts.texts_service.get_all_recitation_texts_by_collection", new_callable=AsyncMock) as mock_get_all_texts, \ + patch("pecha_api.texts.texts_service.TextUtils.filter_text_base_on_group_id_type_and_language_preference", new_callable=AsyncMock) as mock_filter: mock_get_all_texts.return_value = mock_texts mock_filter.return_value = mock_filtered_result @@ -1893,2718 +352,476 @@ async def test_get_root_text_by_collection_id_multiple_groups(): created_date="2025-03-20 09:31:00.000000", updated_date="2025-03-20 09:31:00.000000", published_date="2025-03-20 09:31:00.000000", - published_by="pecha", - categories=[], - views=0 - ) - ] - - # Mock filters for each group - def mock_filter_side_effect(texts, language): - if texts[0].group_id == group_id_1: - return {"root_text": texts[0], "versions": [texts[1]]} - elif texts[0].group_id == group_id_2: - return {"root_text": texts[0], "versions": [texts[1]]} - return {"root_text": None, "versions": texts} - - with patch("pecha_api.texts.texts_service.get_all_recitation_texts_by_collection", new_callable=AsyncMock) as mock_get_all_texts, \ - patch("pecha_api.texts.texts_service.TextUtils.filter_text_base_on_group_id_type_and_language_preference", new_callable=AsyncMock) as mock_filter: - - mock_get_all_texts.return_value = mock_texts - mock_filter.side_effect = mock_filter_side_effect - - result = await get_root_text_by_collection_id(collection_id=collection_id, language=language) - - mock_get_all_texts.assert_called_once_with(collection_id=collection_id, language=language) - - assert result is not None - assert isinstance(result, RecitationsResponse) - assert len(result.recitations) == 2 - assert isinstance(result.recitations[0], RecitationDTO) - assert isinstance(result.recitations[1], RecitationDTO) - # Check that we got both root texts - text_ids = {str(rec.text_id) for rec in result.recitations} - assert text_id_1 in text_ids - assert text_id_3 in text_ids - -@pytest.mark.asyncio -async def test_get_table_of_content_by_type_text_type(): - """Test get_table_of_content_by_type with TEXT type""" - text_id = "text_id_1" - - incoming_toc = TableOfContent( - id="toc_id_1", - text_id=text_id, - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_id_1", - title="section_1", - section_number=1, - parent_id="parent_id_1", - segments=[ - TextSegment(segment_id="pseg_1", segment_number=1) - ], - sections=[], - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652" - ) - ] - ) - - expected_toc = TableOfContent( - text_id=text_id, - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_id_1", - title="section_1", - section_number=1, - segments=[TextSegment(segment_id="seg_id_1", segment_number=1)] - ) - ] - ) - - with patch("pecha_api.texts.texts_service.get_segments_by_text_id", new_callable=AsyncMock) as mock_get_segments: - mock_get_segments.return_value = [type("Seg", (), {"id": "seg_id_1", "pecha_segment_id": "pseg_1"})()] - - result = await get_table_of_content_by_type(table_of_content=incoming_toc) - - assert result is not None - assert isinstance(result, TableOfContent) - assert result.text_id == text_id - assert result.type == TableOfContentType.TEXT - assert len(result.sections) == 1 - assert result.sections[0].segments[0].segment_id == "seg_id_1" - -@pytest.mark.asyncio -async def test_get_table_of_content_by_type_sheet_type(): - """Test get_table_of_content_by_type with SHEET type""" - text_id = "text_id_1" - - incoming_toc = TableOfContent( - id="toc_id_1", - text_id=text_id, - type=TableOfContentType.SHEET, - sections=[ - Section( - id="section_id_1", - title="section_1", - section_number=1, - parent_id="parent_id_1", - segments=[ - TextSegment(segment_id="seg_1", segment_number=1) - ], - sections=[], - created_date="2025-03-16 04:40:54.757652", - updated_date="2025-03-16 04:40:54.757652", - published_date="2025-03-16 04:40:54.757652" - ) - ] - ) - - result = await get_table_of_content_by_type(table_of_content=incoming_toc) - - assert result is not None - assert isinstance(result, TableOfContent) - assert result.text_id == text_id - assert result.type == TableOfContentType.SHEET - assert result.sections == incoming_toc.sections - -def test_get_trimmed_segment_dict_next_direction(): - """Test _get_trimmed_segment_dict_ with NEXT direction""" - from pecha_api.texts.texts_service import _get_trimmed_segment_dict_ - - segments_with_position = [ - ("seg_1", 1), - ("seg_2", 2), - ("seg_3", 3), - ("seg_4", 4), - ("seg_5", 5) - ] - - result = _get_trimmed_segment_dict_( - segments_with_position=segments_with_position, - segment_id="seg_2", - direction=PaginationDirection.NEXT, - size=2 - ) - - assert result is not None - assert isinstance(result, dict) - assert len(result) == 2 - assert "seg_2" in result - assert "seg_3" in result - assert result["seg_2"] == 2 - assert result["seg_3"] == 3 - -def test_get_trimmed_segment_dict_previous_direction(): - """Test _get_trimmed_segment_dict_ with PREVIOUS direction""" - from pecha_api.texts.texts_service import _get_trimmed_segment_dict_ - - segments_with_position = [ - ("seg_1", 1), - ("seg_2", 2), - ("seg_3", 3), - ("seg_4", 4), - ("seg_5", 5) - ] - - result = _get_trimmed_segment_dict_( - segments_with_position=segments_with_position, - segment_id="seg_4", - direction=PaginationDirection.PREVIOUS, - size=2 - ) - - assert result is not None - assert isinstance(result, dict) - assert len(result) == 2 - assert "seg_3" in result - assert "seg_4" in result - assert result["seg_3"] == 3 - assert result["seg_4"] == 4 - -def test_get_trimmed_segment_dict_next_at_end(): - """Test _get_trimmed_segment_dict_ with NEXT direction at end of list""" - from pecha_api.texts.texts_service import _get_trimmed_segment_dict_ - - segments_with_position = [ - ("seg_1", 1), - ("seg_2", 2), - ("seg_3", 3) - ] - - result = _get_trimmed_segment_dict_( - segments_with_position=segments_with_position, - segment_id="seg_2", - direction=PaginationDirection.NEXT, - size=5 - ) - - assert result is not None - assert isinstance(result, dict) - assert len(result) == 2 - assert "seg_2" in result - assert "seg_3" in result - -def test_get_trimmed_segment_dict_previous_at_start(): - """Test _get_trimmed_segment_dict_ with PREVIOUS direction at start of list""" - from pecha_api.texts.texts_service import _get_trimmed_segment_dict_ - - segments_with_position = [ - ("seg_1", 1), - ("seg_2", 2), - ("seg_3", 3) - ] - - result = _get_trimmed_segment_dict_( - segments_with_position=segments_with_position, - segment_id="seg_1", - direction=PaginationDirection.PREVIOUS, - size=5 - ) - - assert result is not None - assert isinstance(result, dict) - assert len(result) == 1 - assert "seg_1" in result - -def test_get_segments_with_position_simple(): - """Test _get_segments_with_position_ with simple sections""" - from pecha_api.texts.texts_service import _get_segments_with_position_ - - table_of_content = TableOfContent( - id="toc_id", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1), - TextSegment(segment_id="seg_2", segment_number=2) - ], - sections=[] - ) - ] - ) - - result = _get_segments_with_position_(table_of_content=table_of_content) - - assert result is not None - assert isinstance(result, list) - assert len(result) == 2 - assert result[0] == ("seg_1", 1) - assert result[1] == ("seg_2", 2) - -def test_get_segments_with_position_nested(): - """Test _get_segments_with_position_ with nested sections""" - from pecha_api.texts.texts_service import _get_segments_with_position_ - - table_of_content = TableOfContent( - id="toc_id", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1) - ], - sections=[ - Section( - id="section_2", - title="Section 2", - section_number=2, - segments=[ - TextSegment(segment_id="seg_2", segment_number=1) - ], - sections=[] - ) - ] - ) - ] - ) - - result = _get_segments_with_position_(table_of_content=table_of_content) - - assert result is not None - assert isinstance(result, list) - assert len(result) == 2 - assert result[0] == ("seg_1", 1) - assert result[1] == ("seg_2", 2) - -def test_filter_single_section_with_wanted_segments(): - """Test _filter_single_section_ with wanted segments""" - from pecha_api.texts.texts_service import _filter_single_section_ - - section = Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1), - TextSegment(segment_id="seg_2", segment_number=2), - TextSegment(segment_id="seg_3", segment_number=3) - ], - sections=[] - ) - - wanted_segment_ids = {"seg_1", "seg_3"} - - result = _filter_single_section_(section=section, wanted_segment_ids=wanted_segment_ids) - - assert result is not None - assert isinstance(result, Section) - assert len(result.segments) == 2 - assert result.segments[0].segment_id == "seg_1" - assert result.segments[1].segment_id == "seg_3" - -def test_filter_single_section_no_wanted_segments(): - """Test _filter_single_section_ with no wanted segments""" - from pecha_api.texts.texts_service import _filter_single_section_ - - section = Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1), - TextSegment(segment_id="seg_2", segment_number=2) - ], - sections=[] - ) - - wanted_segment_ids = {"seg_3", "seg_4"} - - result = _filter_single_section_(section=section, wanted_segment_ids=wanted_segment_ids) - - assert result is None - -def test_filter_single_section_nested_with_wanted_segments(): - """Test _filter_single_section_ with nested sections containing wanted segments""" - from pecha_api.texts.texts_service import _filter_single_section_ - - section = Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1) - ], - sections=[ - Section( - id="section_2", - title="Section 2", - section_number=2, - segments=[ - TextSegment(segment_id="seg_2", segment_number=1) - ], - sections=[] - ) - ] - ) - - wanted_segment_ids = {"seg_2"} - - result = _filter_single_section_(section=section, wanted_segment_ids=wanted_segment_ids) - - assert result is not None - assert isinstance(result, Section) - assert len(result.segments) == 0 # Parent has no wanted segments - assert len(result.sections) == 1 # But subsection has wanted segments - assert result.sections[0].segments[0].segment_id == "seg_2" - -def test_generate_paginated_table_of_content_by_segments(): - """Test _generate_paginated_table_of_content_by_segments_""" - from pecha_api.texts.texts_service import _generate_paginated_table_of_content_by_segments_ - - table_of_content = TableOfContent( - id="toc_id", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1), - TextSegment(segment_id="seg_2", segment_number=2), - TextSegment(segment_id="seg_3", segment_number=3) - ], - sections=[] - ) - ] - ) - - segment_dict = { - "seg_1": 1, - "seg_3": 3 - } - - result = _generate_paginated_table_of_content_by_segments_( - table_of_content=table_of_content, - segment_dict=segment_dict - ) - - assert result is not None - assert isinstance(result, TableOfContent) - assert result.type == TableOfContentType.TEXT - assert len(result.sections) == 1 - assert len(result.sections[0].segments) == 2 - assert result.sections[0].segments[0].segment_id == "seg_1" - assert result.sections[0].segments[1].segment_id == "seg_3" - -def test_generate_paginated_table_of_content_by_segments_with_sheet_type(): - """Test _generate_paginated_table_of_content_by_segments_ preserves SHEET type""" - from pecha_api.texts.texts_service import _generate_paginated_table_of_content_by_segments_ - - table_of_content = TableOfContent( - id="toc_id", - text_id="text_id", - type=TableOfContentType.SHEET, - sections=[ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1), - TextSegment(segment_id="seg_2", segment_number=2) - ], - sections=[] - ) - ] - ) - - segment_dict = { - "seg_1": 1 - } - - result = _generate_paginated_table_of_content_by_segments_( - table_of_content=table_of_content, - segment_dict=segment_dict - ) - - assert result is not None - assert isinstance(result, TableOfContent) - assert result.type == TableOfContentType.SHEET - assert len(result.sections) == 1 - assert len(result.sections[0].segments) == 1 - assert result.sections[0].segments[0].segment_id == "seg_1" - -def test_search_section_found(): - """Test _search_section_ when segment is found""" - from pecha_api.texts.texts_service import _search_section_ - - sections = [ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1), - TextSegment(segment_id="seg_2", segment_number=2) - ], - sections=[] - ) - ] - - result = _search_section_(sections=sections, segment_id="seg_2") - - assert result is True - -def test_search_section_not_found(): - """Test _search_section_ when segment is not found""" - from pecha_api.texts.texts_service import _search_section_ - - sections = [ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1) - ], - sections=[] - ) - ] - - result = _search_section_(sections=sections, segment_id="seg_3") - - assert result is False - -def test_search_section_nested(): - """Test _search_section_ with nested sections""" - from pecha_api.texts.texts_service import _search_section_ - - sections = [ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1) - ], - sections=[ - Section( - id="section_2", - title="Section 2", - section_number=2, - segments=[ - TextSegment(segment_id="seg_2", segment_number=1) - ], - sections=[] - ) - ] - ) - ] - - result = _search_section_(sections=sections, segment_id="seg_2") - - assert result is True - -def test_search_table_of_content_where_segment_id_exists_found(): - """Test _search_table_of_content_where_segment_id_exists when segment is found""" - from pecha_api.texts.texts_service import _search_table_of_content_where_segment_id_exists - - table_of_contents = [ - TableOfContent( - id="toc_1", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1) - ], - sections=[] - ) - ] - ), - TableOfContent( - id="toc_2", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_2", - title="Section 2", - section_number=1, - segments=[ - TextSegment(segment_id="seg_2", segment_number=1) - ], - sections=[] - ) - ] - ) - ] - - result = _search_table_of_content_where_segment_id_exists( - table_of_contents=table_of_contents, - segment_id="seg_2" - ) - - assert result is not None - assert isinstance(result, TableOfContent) - assert result.id == "toc_2" - -def test_search_table_of_content_where_segment_id_exists_not_found(): - """Test _search_table_of_content_where_segment_id_exists when segment is not found""" - from pecha_api.texts.texts_service import _search_table_of_content_where_segment_id_exists - - table_of_contents = [ - TableOfContent( - id="toc_1", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1) - ], - sections=[] - ) - ] - ) - ] - - with pytest.raises(HTTPException) as exc_info: - _search_table_of_content_where_segment_id_exists( - table_of_contents=table_of_contents, - segment_id="seg_3" - ) - - assert exc_info.value.status_code == 404 - assert exc_info.value.detail == ErrorConstants.TABLE_OF_CONTENT_NOT_FOUND_MESSAGE - -def test_get_first_segment_and_table_of_content_success(): - """Test _get_first_segment_and_table_of_content_ when segment is found""" - from pecha_api.texts.texts_service import _get_first_segment_and_table_of_content_ - - table_of_contents = [ - TableOfContent( - id="toc_1", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1) - ], - sections=[] - ) - ] - ) - ] - - segment_id, table_of_content = _get_first_segment_and_table_of_content_( - table_of_contents=table_of_contents - ) - - assert segment_id == "seg_1" - assert table_of_content is not None - assert table_of_content.id == "toc_1" - -def test_get_first_segment_and_table_of_content_no_segments(): - """Test _get_first_segment_and_table_of_content_ when no segments exist""" - from pecha_api.texts.texts_service import _get_first_segment_and_table_of_content_ - - table_of_contents = [ - TableOfContent( - id="toc_1", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[], - sections=[] - ) - ] - ) - ] - - segment_id, table_of_content = _get_first_segment_and_table_of_content_( - table_of_contents=table_of_contents - ) - - assert segment_id is None - assert table_of_content is None - -@pytest.mark.asyncio -async def test_receive_table_of_content_with_content_and_segment_id(): - """Test _receive_table_of_content with content_id and segment_id""" - from pecha_api.texts.texts_service import _receive_table_of_content - - text_details_request = TextDetailsRequest(content_id="content_1", segment_id="seg_1") - mock_table_of_content = TableOfContent( - id="toc_1", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1) - ], - sections=[] - ) - ] - ) - - with patch( - "pecha_api.texts.texts_service.get_table_of_content_by_content_id", - new_callable=AsyncMock, - return_value=mock_table_of_content - ) as mock_get_table_of_content_by_content_id, patch( - "pecha_api.texts.texts_service.get_contents_by_id", - new_callable=AsyncMock - ) as mock_get_contents_by_id: - result = await _receive_table_of_content(text_id="text_id", text_details_request=text_details_request) - - assert result == mock_table_of_content - mock_get_table_of_content_by_content_id.assert_awaited_once_with(content_id="content_1") - mock_get_contents_by_id.assert_not_awaited() - -@pytest.mark.asyncio -async def test_receive_table_of_content_with_segment_id_only(): - """Test _receive_table_of_content with only segment_id""" - from pecha_api.texts.texts_service import _receive_table_of_content - - text_details_request = TextDetailsRequest(segment_id="seg_2") - mock_table_of_contents = [ - TableOfContent( - id="toc_1", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[] - ) - ] - expected_table_of_content = TableOfContent( - id="toc_2", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[] - ) - - with patch( - "pecha_api.texts.texts_service.get_contents_by_id", - new_callable=AsyncMock, - return_value=mock_table_of_contents - ) as mock_get_contents_by_id, patch( - "pecha_api.texts.texts_service._search_table_of_content_where_segment_id_exists", - return_value=expected_table_of_content - ) as mock_search_table_of_content: - result = await _receive_table_of_content(text_id="text_id", text_details_request=text_details_request) - - assert result == expected_table_of_content - mock_get_contents_by_id.assert_awaited_once_with(text_id="text_id") - mock_search_table_of_content.assert_called_once_with( - table_of_contents=mock_table_of_contents, - segment_id="seg_2" - ) - -@pytest.mark.asyncio -async def test_receive_table_of_content_without_segment_and_content_id(): - """Test _receive_table_of_content when no content_id or segment_id is provided""" - from pecha_api.texts.texts_service import _receive_table_of_content - - text_details_request = TextDetailsRequest() - mock_table_of_contents = [ - TableOfContent( - id="toc_1", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[] - ) - ] - expected_table_of_content = TableOfContent( - id="toc_1", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[] - ) - - with patch( - "pecha_api.texts.texts_service.get_contents_by_id", - new_callable=AsyncMock, - return_value=mock_table_of_contents - ) as mock_get_contents_by_id, patch( - "pecha_api.texts.texts_service._get_first_segment_and_table_of_content_", - return_value=("seg_1", expected_table_of_content) - ) as mock_get_first_segment_and_table_of_content: - result = await _receive_table_of_content(text_id="text_id", text_details_request=text_details_request) - - assert result == expected_table_of_content - assert text_details_request.segment_id == "seg_1" - mock_get_contents_by_id.assert_awaited_once_with(text_id="text_id") - mock_get_first_segment_and_table_of_content.assert_called_once_with( - table_of_contents=mock_table_of_contents - ) - -@pytest.mark.asyncio -async def test_receive_table_of_content_not_found(): - """Test _receive_table_of_content when table of content is not found""" - from pecha_api.texts.texts_service import _receive_table_of_content - - text_details_request = TextDetailsRequest(content_id="content_1", segment_id="seg_1") - - with patch( - "pecha_api.texts.texts_service.get_table_of_content_by_content_id", - new_callable=AsyncMock, - return_value=None - ): - with pytest.raises(HTTPException) as exc_info: - await _receive_table_of_content(text_id="text_id", text_details_request=text_details_request) - - assert exc_info.value.status_code == 404 - assert exc_info.value.detail == ErrorConstants.TABLE_OF_CONTENT_NOT_FOUND_MESSAGE - -def test_get_paginated_sections(): - """Test _get_paginated_sections""" - from pecha_api.texts.texts_service import _get_paginated_sections - - sections = [ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1), - TextSegment(segment_id="seg_2", segment_number=2) - ], - sections=[] - ), - Section( - id="section_2", - title="Section 2", - section_number=2, - segments=[ - TextSegment(segment_id="seg_3", segment_number=1) - ], - sections=[] - ), - Section( - id="section_3", - title="Section 3", - section_number=3, - segments=[ - TextSegment(segment_id="seg_4", segment_number=1) - ], - sections=[] - ) - ] - - result = _get_paginated_sections(sections=sections, skip=0, limit=2) - - assert result is not None - assert isinstance(result, list) - assert len(result) == 2 - assert result[0].id == "section_1" - assert result[1].id == "section_2" - # Each section should only have first segment - assert len(result[0].segments) == 1 - assert result[0].segments[0].segment_id == "seg_1" - -def test_get_paginated_sections_with_skip(): - """Test _get_paginated_sections with skip""" - from pecha_api.texts.texts_service import _get_paginated_sections - - sections = [ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="seg_1", segment_number=1) - ], - sections=[] - ), - Section( - id="section_2", - title="Section 2", - section_number=2, - segments=[ - TextSegment(segment_id="seg_2", segment_number=1) - ], - sections=[] - ) - ] - - result = _get_paginated_sections(sections=sections, skip=1, limit=2) - - assert result is not None - assert isinstance(result, list) - assert len(result) == 1 - assert result[0].id == "section_2" - -@pytest.mark.asyncio -async def test_update_text_details_cache_update_fails(): - """Test update_text_details when cache update fails""" - mock_text_details = TextDTO( - id="text_id_1", - title="text_title", - language="bo", - group_id="group_id_1", - type="version", - is_published=False, - created_date="created_date", - updated_date="updated_date", - published_date="published_date", - published_by="published_by", - categories=[], - views=0 - ) - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_details), \ - patch("pecha_api.texts.texts_service.update_text_details_by_id", new_callable=AsyncMock, return_value=mock_text_details), \ - patch("pecha_api.texts.texts_service.update_text_details_cache", new_callable=AsyncMock, side_effect=Exception("Cache error")), \ - patch("pecha_api.texts.texts_service.invalidate_text_cache_on_update", new_callable=AsyncMock, return_value=None) as mock_invalidate: - - response = await update_text_details(text_id="text_id_1", update_text_request=UpdateTextRequest(title="updated_title", is_published=True)) - - assert response is not None - # Verify that invalidate was called as fallback - mock_invalidate.assert_called_once_with(text_id="text_id_1") - -@pytest.mark.asyncio -async def test_get_table_of_content_by_sheet_id_with_cache(): - """Test get_table_of_content_by_sheet_id when data is in cache""" - sheet_id = "sheet_id_1" - cached_toc = TableOfContent( - id="content_id_1", - text_id=sheet_id, - type=TableOfContentType.SHEET, - sections=[ - Section( - id="section_id_1", - title="section_title", - section_number=1, - parent_id="parent_id_1", - segments=[ - TextSegment( - segment_id="segment_id_1", - segment_number=1 - ) - ], - sections=[], - created_date="created_date", - updated_date="updated_date", - published_date="published_date" - ) - ] - ) - - with patch("pecha_api.texts.texts_service.get_table_of_content_by_sheet_id_cache", new_callable=AsyncMock, return_value=cached_toc): - response = await get_table_of_content_by_sheet_id(sheet_id=sheet_id) - - assert response is not None - assert isinstance(response, TableOfContent) - assert response.id == "content_id_1" - -@pytest.mark.asyncio -async def test_get_table_of_content_by_sheet_id_no_content(): - """Test get_table_of_content_by_sheet_id when no content exists""" - sheet_id = "sheet_id_1" - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=[]), \ - patch("pecha_api.texts.texts_service.get_table_of_content_by_sheet_id_cache", new_callable=AsyncMock, return_value=None): - - response = await get_table_of_content_by_sheet_id(sheet_id=sheet_id) - - assert response is None - - -@pytest.mark.asyncio -async def test_get_text_by_text_id_or_collection_with_cache(): - """Test get_text_by_text_id_or_collection fetches text (cache is currently disabled in code)""" - text_id = "efb26a06-f373-450b-ba57-e7a8d4dd5b64" - mock_text = TextDTO( - id=text_id, - title="Test Text", - language="bo", - group_id="group_id_1", - type="commentary", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0 - ) - - # Cache is commented out in the actual code, so we need to mock the actual data fetch - with patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text), \ - patch("pecha_api.texts.texts_service.set_text_by_text_id_or_collection_cache", new_callable=AsyncMock): - response = await get_text_by_text_id_or_collection(text_id=text_id, collection_id=None) - - assert response is not None - assert isinstance(response, TextDTO) - assert response.id == text_id - assert response.title == "Test Text" - - -@pytest.mark.asyncio -async def test_get_sheet_with_filters(): - """Test get_sheet with various filter parameters""" - mock_sheets = [ - TextDTO( - id="sheet_id_1", - title="Sheet 1", - language="bo", - group_id="group_id_1", - type="sheet", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="user_1", - categories=[], - views=10 - ) - ] - - with patch("pecha_api.texts.texts_service.fetch_sheets_from_db", new_callable=AsyncMock, return_value=mock_sheets): - result = await get_sheet( - published_by="user_1", - is_published=True, - sort_by=SortBy.CREATED_DATE, - sort_order=SortOrder.DESC, - skip=0, - limit=10 - ) - - assert result is not None - assert isinstance(result, list) - assert len(result) == 1 - assert result[0].id == "sheet_id_1" - - -@pytest.mark.asyncio -async def test_update_text_details_with_cache_invalidation(): - """Test update_text_details updates text successfully""" - text_id = "123e4567-e89b-12d3-a456-426614174000" - update_request = UpdateTextRequest( - title="Updated Title", - language="bo" - ) - - updated_text = TextDTO( - id=text_id, - title="Updated Title", - language="bo", - group_id="group_id_1", - type="version", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0 - ) - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=updated_text), \ - patch("pecha_api.texts.texts_service.update_text_details_by_id", new_callable=AsyncMock, return_value=updated_text) as mock_update, \ - patch("pecha_api.texts.texts_service.invalidate_text_cache_on_update", new_callable=AsyncMock): - - result = await update_text_details(text_id=text_id, update_text_request=update_request) - - assert result is not None - assert result.title == "Updated Title" - mock_update.assert_called_once_with(text_id=text_id, update_text_request=update_request) - - -@pytest.mark.asyncio -async def test_remove_table_of_content_by_text_id_success(): - """Test remove_table_of_content_by_text_id successfully deletes content""" - text_id = "123e4567-e89b-12d3-a456-426614174000" - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.delete_table_of_content_by_text_id", new_callable=AsyncMock) as mock_delete: - await remove_table_of_content_by_text_id(text_id=text_id) - - mock_delete.assert_called_once_with(text_id=text_id) - - -@pytest.mark.asyncio -async def test_get_table_of_content_by_sheet_id_with_cache(): - """Test get_table_of_content_by_sheet_id returns cached data""" - sheet_id = "sheet_id_1" - cached_toc = TableOfContent( - id="toc_id_1", - text_id=sheet_id, - type=TableOfContentType.SHEET, - sections=[] - ) - - with patch("pecha_api.texts.texts_service.get_table_of_content_by_sheet_id_cache", new_callable=AsyncMock, return_value=cached_toc): - result = await get_table_of_content_by_sheet_id(sheet_id=sheet_id) - - assert result is not None - assert isinstance(result, TableOfContent) - assert result.id == "toc_id_1" - - -@pytest.mark.asyncio -async def test_get_text_by_collection_id_empty_result(): - """Test get_text_by_text_id_or_collection with collection_id returns empty when no texts""" - collection_id = "collection_id_1" - language = "bo" - - mock_collection = CollectionModel( - id=collection_id, - title="Empty Collection", - description="No texts", - language=language, - slug="empty-collection", - has_child=False - ) - - with patch("pecha_api.texts.texts_service.get_text_by_text_id_or_collection_cache", new_callable=AsyncMock, return_value=None), \ - patch("pecha_api.texts.texts_service.get_collection", new_callable=AsyncMock, return_value=mock_collection), \ - patch("pecha_api.texts.texts_service._get_texts_by_collection_id", new_callable=AsyncMock, return_value=([], 0)), \ - patch("pecha_api.texts.texts_service.get_all_texts_by_collection", new_callable=AsyncMock, return_value=[]), \ - patch("pecha_api.texts.texts_service.set_text_by_text_id_or_collection_cache", new_callable=AsyncMock): - - response = await get_text_by_text_id_or_collection( - text_id=None, - collection_id=collection_id, - language=language, - skip=0, - limit=10 - ) - - assert response is not None - assert isinstance(response, TextsCategoryResponse) - assert response.total == 0 - - -# Tests for new private functions - -def test_group_texts_by_group_id_with_language_sorting(): - """Test _group_texts_by_group_id groups texts and sorts by language preference""" - from pecha_api.texts.texts_service import _group_texts_by_group_id - - # Create mock texts with different group_ids and languages - mock_texts = [ - TextDTO( - id="text_id_1", - title="Text 1 Bo", - language="bo", - group_id="group_1", - type="version", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0 - ), - TextDTO( - id="text_id_2", - title="Text 2 En", - language="en", - group_id="group_1", - type="version", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0 - ), - TextDTO( - id="text_id_3", - title="Text 3 Zh", - language="zh", - group_id="group_1", - type="version", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0 - ), - TextDTO( - id="text_id_4", - title="Text 4 Bo Group2", - language="bo", - group_id="group_2", - type="version", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0 - ) - ] - - # Test with 'en' as preferred language - result = _group_texts_by_group_id(texts=mock_texts, language="en") - - # Verify grouping - assert len(result) == 2 - assert "group_1" in result - assert "group_2" in result - assert len(result["group_1"]) == 3 - assert len(result["group_2"]) == 1 - - # Verify sorting - 'en' should be first for group_1 - assert result["group_1"][0].language == "en" - assert result["group_1"][1].language == "bo" - assert result["group_1"][2].language == "zh" - - -def test_group_texts_by_group_id_with_bo_preference(): - """Test _group_texts_by_group_id with Tibetan (bo) language preference""" - from pecha_api.texts.texts_service import _group_texts_by_group_id - - mock_texts = [ - TextDTO( - id="text_id_1", - title="Text En", - language="en", - group_id="group_1", - type="version", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0 - ), - TextDTO( - id="text_id_2", - title="Text Bo", - language="bo", - group_id="group_1", - type="version", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0 - ) - ] - - result = _group_texts_by_group_id(texts=mock_texts, language="bo") - - # Verify 'bo' is first - assert result["group_1"][0].language == "bo" - assert result["group_1"][1].language == "en" - - -def test_get_language_priority_via_textutils(): - """Test language priority through TextUtils (used by service layer)""" - from pecha_api.texts.texts_utils import TextUtils - - # Test with 'bo' preference - assert TextUtils.get_language_priority("bo", "bo") == 0 - assert TextUtils.get_language_priority("en", "bo") == 1 - assert TextUtils.get_language_priority("zh", "bo") == 2 - assert TextUtils.get_language_priority("unknown", "bo") == 999 - - # Test with 'en' preference - assert TextUtils.get_language_priority("en", "en") == 0 - assert TextUtils.get_language_priority("bo", "en") == 1 - assert TextUtils.get_language_priority("zh", "en") == 2 - assert TextUtils.get_language_priority("unknown", "en") == 999 - - # Test with 'zh' preference - assert TextUtils.get_language_priority("zh", "zh") == 0 - assert TextUtils.get_language_priority("en", "zh") == 1 - assert TextUtils.get_language_priority("bo", "zh") == 2 - - -def test_get_language_priority_with_none_via_textutils(): - """Test TextUtils.get_language_priority handles None text_language""" - from pecha_api.texts.texts_utils import TextUtils - - # None language should get default priority - assert TextUtils.get_language_priority(None, "bo") == 999 - assert TextUtils.get_language_priority(None, "en") == 999 - - -def test_group_texts_by_group_id_empty_texts(): - """Test _group_texts_by_group_id with empty text list""" - from pecha_api.texts.texts_service import _group_texts_by_group_id - - result = _group_texts_by_group_id(texts=[], language="bo") - - assert result == {} - - -def test_group_texts_by_group_id_single_group(): - """Test _group_texts_by_group_id with single group""" - from pecha_api.texts.texts_service import _group_texts_by_group_id - - mock_texts = [ - TextDTO( - id="text_id_1", - title="Text 1", - language="bo", - group_id="single_group", - type="version", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0 - ) - ] - - result = _group_texts_by_group_id(texts=mock_texts, language="bo") - - assert len(result) == 1 - assert "single_group" in result - assert len(result["single_group"]) == 1 - assert result["single_group"][0].id == "text_id_1" - - -def test_language_orders_constant(): - """Test LANGUAGE_ORDERS constant is properly defined""" - # Verify the constant exists and has expected structure - assert LANGUAGE_ORDERS is not None - assert isinstance(LANGUAGE_ORDERS, dict) - - # Check all three languages are defined - assert 'bo' in LANGUAGE_ORDERS - assert 'en' in LANGUAGE_ORDERS - assert 'zh' in LANGUAGE_ORDERS - - # Verify 'bo' preference order - assert LANGUAGE_ORDERS['bo']['bo'] == 0 - assert LANGUAGE_ORDERS['bo']['en'] == 1 - assert LANGUAGE_ORDERS['bo']['zh'] == 2 - - # Verify 'en' preference order - assert LANGUAGE_ORDERS['en']['en'] == 0 - assert LANGUAGE_ORDERS['en']['bo'] == 1 - assert LANGUAGE_ORDERS['en']['zh'] == 2 - - # Verify 'zh' preference order - assert LANGUAGE_ORDERS['zh']['zh'] == 0 - assert LANGUAGE_ORDERS['zh']['en'] == 1 - assert LANGUAGE_ORDERS['zh']['bo'] == 2 - - -@pytest.mark.asyncio -async def test_get_commentaries_by_text_id_success(): - """Test get_commentaries_by_text_id returns matching commentaries""" - from uuid import UUID - - text_id = "text_id_1" - group_id = "group_id_1" - - mock_root_text = TextDTO( - id=text_id, - title="Root Text", - language="bo", - group_id=group_id, - type="version", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0 - ) - - mock_commentaries = [ - TextDTO( - id="commentary_id_1", - title="Commentary 1", - language="bo", - group_id="commentary_group_1", - type="commentary", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[group_id], # Commentary references the root text's group_id - views=0 - ), - TextDTO( - id="commentary_id_2", - title="Commentary 2", - language="bo", - group_id="commentary_group_2", - type="commentary", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=["other_group"], # This one shouldn't match - views=0 - ) - ] - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_root_text), \ - patch("pecha_api.texts.texts_service.TextUtils.get_commentaries_by_text_type", new_callable=AsyncMock, return_value=mock_commentaries): - - result = await get_commentaries_by_text_id(text_id=text_id, skip=0, limit=10) - - assert result is not None - assert len(result) == 1 - assert result[0].id == "commentary_id_1" - assert group_id in result[0].categories - - -@pytest.mark.asyncio -async def test_get_commentaries_by_text_id_no_matching_commentaries(): - """Test get_commentaries_by_text_id with no matching commentaries""" - text_id = "text_id_1" - group_id = "group_id_1" - - mock_root_text = TextDTO( - id=text_id, - title="Root Text", - language="bo", - group_id=group_id, - type="version", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0 - ) - - mock_commentaries = [ - TextDTO( - id="commentary_id_1", - title="Commentary 1", - language="bo", - group_id="commentary_group_1", - type="commentary", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=["other_group"], # Different group - views=0 - ) - ] - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_root_text), \ - patch("pecha_api.texts.texts_service.TextUtils.get_commentaries_by_text_type", new_callable=AsyncMock, return_value=mock_commentaries): - - result = await get_commentaries_by_text_id(text_id=text_id, skip=0, limit=10) - - assert result is not None - assert len(result) == 0 - - -@pytest.mark.asyncio -async def test_get_commentaries_by_text_id_invalid_text(): - """Test get_commentaries_by_text_id raises exception for invalid text""" - text_id = "invalid_text_id" - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, side_effect=HTTPException(status_code=404, detail="Text not found")): - with pytest.raises(HTTPException) as exc_info: - await get_commentaries_by_text_id(text_id=text_id, skip=0, limit=10) - - assert exc_info.value.status_code == 404 - - -@pytest.mark.asyncio -async def test_replace_pecha_segment_id_with_segment_id_success(): - """Test replace_pecha_segment_id_with_segment_id converts pecha segment IDs to database segment IDs""" - text_id = "text_id_1" - - # Mock segments from database - class MockSegment: - def __init__(self, segment_id, pecha_segment_id): - self.id = segment_id - self.pecha_segment_id = pecha_segment_id - - mock_segments = [ - MockSegment("db_seg_1", "pecha_seg_1"), - MockSegment("db_seg_2", "pecha_seg_2") - ] - - incoming_toc = TableOfContent( - text_id=text_id, - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="pecha_seg_1", segment_number=1), - TextSegment(segment_id="pecha_seg_2", segment_number=2) - ] - ) - ] - ) - - with patch("pecha_api.texts.texts_service.get_segments_by_text_id", new_callable=AsyncMock, return_value=mock_segments): - result = await replace_pecha_segment_id_with_segment_id(table_of_content=incoming_toc) - - assert result is not None - assert isinstance(result, TableOfContent) - assert result.text_id == text_id - assert result.type == TableOfContentType.TEXT - assert len(result.sections) == 1 - assert len(result.sections[0].segments) == 2 - # Verify segment IDs were replaced - assert result.sections[0].segments[0].segment_id == "db_seg_1" - assert result.sections[0].segments[1].segment_id == "db_seg_2" - - -@pytest.mark.asyncio -async def test_replace_pecha_segment_id_with_segment_id_multiple_sections(): - """Test replace_pecha_segment_id_with_segment_id with multiple sections""" - text_id = "text_id_1" - - class MockSegment: - def __init__(self, segment_id, pecha_segment_id): - self.id = segment_id - self.pecha_segment_id = pecha_segment_id - - mock_segments = [ - MockSegment("db_seg_1", "pecha_seg_1"), - MockSegment("db_seg_2", "pecha_seg_2"), - MockSegment("db_seg_3", "pecha_seg_3") - ] - - incoming_toc = TableOfContent( - text_id=text_id, - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[ - TextSegment(segment_id="pecha_seg_1", segment_number=1) - ] - ), - Section( - id="section_2", - title="Section 2", - section_number=2, - segments=[ - TextSegment(segment_id="pecha_seg_2", segment_number=1), - TextSegment(segment_id="pecha_seg_3", segment_number=2) - ] - ) - ] - ) - - with patch("pecha_api.texts.texts_service.get_segments_by_text_id", new_callable=AsyncMock, return_value=mock_segments): - result = await replace_pecha_segment_id_with_segment_id(table_of_content=incoming_toc) - - assert result is not None - assert len(result.sections) == 2 - assert result.sections[0].segments[0].segment_id == "db_seg_1" - assert result.sections[1].segments[0].segment_id == "db_seg_2" - assert result.sections[1].segments[1].segment_id == "db_seg_3" - - -@pytest.mark.asyncio -async def test_get_text_by_pecha_text_ids_service_success(): - """Test get_text_by_pecha_text_ids_service with valid pecha_text_ids""" - class MockText: - def __init__(self, text_id, pecha_text_id, title, language, group_id, type_val, is_published, - created_date, updated_date, published_date, published_by, categories=None, - views=0, source_link=None, ranking=None, license_val=None): - self.id = text_id - self.pecha_text_id = pecha_text_id - self.title = title - self.language = language - self.group_id = group_id - self.type = type_val - self.is_published = is_published - self.created_date = created_date - self.updated_date = updated_date - self.published_date = published_date - self.published_by = published_by - self.categories = categories or [] - self.views = views - self.source_link = source_link - self.ranking = ranking - self.license = license_val - - mock_texts = [ - MockText( - text_id="text_id_1", - pecha_text_id="pecha_text_id_1", - title="Test Text 1", - language="bo", - group_id="group_id_1", - type_val="root_text", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="user_1", - categories=["cat1"], - views=10, - source_link="https://source1.com", - ranking=1, - license_val="CC0" - ), - MockText( - text_id="text_id_2", - pecha_text_id="pecha_text_id_2", - title="Test Text 2", - language="en", - group_id="group_id_2", - type_val="version", - is_published=True, - created_date="2025-01-02T00:00:00", - updated_date="2025-01-02T00:00:00", - published_date="2025-01-02T00:00:00", - published_by="user_2", - categories=["cat2"], - views=20, - source_link="https://source2.com", - ranking=2, - license_val="CC BY" - ) - ] - - request = TextsByPechaTextIdsRequest(pecha_text_ids=["pecha_text_id_1", "pecha_text_id_2"]) - - with patch("pecha_api.texts.texts_service.get_texts_by_pecha_text_ids", new_callable=AsyncMock, return_value=mock_texts): - result = await get_text_by_pecha_text_ids_service(texts_by_pecha_text_ids_request=request) - - assert result is not None - assert len(result) == 2 - assert isinstance(result[0], TextDTO) - assert result[0].id == "text_id_1" - assert result[0].pecha_text_id == "pecha_text_id_1" - assert result[0].title == "Test Text 1" - assert result[0].language == "bo" - assert result[0].group_id == "group_id_1" - assert result[0].type == "root_text" - assert result[0].is_published == True - assert result[0].categories == ["cat1"] - assert result[0].views == 10 - assert result[0].source_link == "https://source1.com" - assert result[0].ranking == 1 - assert result[0].license == "CC0" - - assert isinstance(result[1], TextDTO) - assert result[1].id == "text_id_2" - assert result[1].pecha_text_id == "pecha_text_id_2" - assert result[1].title == "Test Text 2" - - -@pytest.mark.asyncio -async def test_get_text_by_pecha_text_ids_service_empty_result(): - """Test get_text_by_pecha_text_ids_service when no texts are found""" - request = TextsByPechaTextIdsRequest(pecha_text_ids=["nonexistent_pecha_id"]) - - with patch("pecha_api.texts.texts_service.get_texts_by_pecha_text_ids", new_callable=AsyncMock, return_value=[]): - result = await get_text_by_pecha_text_ids_service(texts_by_pecha_text_ids_request=request) - - assert result is not None - assert len(result) == 0 - assert result == [] - - -@pytest.mark.asyncio -async def test_get_text_by_pecha_text_ids_service_single_text(): - """Test get_text_by_pecha_text_ids_service with single pecha_text_id""" - class MockText: - def __init__(self): - self.id = "text_id_single" - self.pecha_text_id = "pecha_text_id_single" - self.title = "Single Test Text" - self.language = "bo" - self.group_id = "group_id_single" - self.type = "root_text" - self.is_published = True - self.created_date = "2025-01-01T00:00:00" - self.updated_date = "2025-01-01T00:00:00" - self.published_date = "2025-01-01T00:00:00" - self.published_by = "user_single" - self.categories = [] - self.views = 5 - self.source_link = "https://source.com" - self.ranking = 1 - self.license = "CC0" - - mock_texts = [MockText()] - request = TextsByPechaTextIdsRequest(pecha_text_ids=["pecha_text_id_single"]) - - with patch("pecha_api.texts.texts_service.get_texts_by_pecha_text_ids", new_callable=AsyncMock, return_value=mock_texts): - result = await get_text_by_pecha_text_ids_service(texts_by_pecha_text_ids_request=request) - - assert result is not None - assert len(result) == 1 - assert isinstance(result[0], TextDTO) - assert result[0].id == "text_id_single" - assert result[0].pecha_text_id == "pecha_text_id_single" - assert result[0].title == "Single Test Text" - - -@pytest.mark.asyncio -async def test_get_text_by_pecha_text_ids_service_with_none_optional_fields(): - """Test get_text_by_pecha_text_ids_service with texts having None optional fields""" - class MockText: - def __init__(self): - self.id = "text_id_minimal" - self.pecha_text_id = None - self.title = "Minimal Text" - self.language = "en" - self.group_id = "group_id_minimal" - self.type = "version" - self.is_published = False - self.created_date = "2025-01-01T00:00:00" - self.updated_date = "2025-01-01T00:00:00" - self.published_date = "2025-01-01T00:00:00" - self.published_by = "user_minimal" - self.categories = None - self.views = 0 - self.source_link = None - self.ranking = None - self.license = None - - mock_texts = [MockText()] - request = TextsByPechaTextIdsRequest(pecha_text_ids=["pecha_text_id_minimal"]) - - with patch("pecha_api.texts.texts_service.get_texts_by_pecha_text_ids", new_callable=AsyncMock, return_value=mock_texts): - result = await get_text_by_pecha_text_ids_service(texts_by_pecha_text_ids_request=request) - - assert result is not None - assert len(result) == 1 - assert isinstance(result[0], TextDTO) - assert result[0].id == "text_id_minimal" - # Note: str(None) returns "None" as a string in the service - assert result[0].pecha_text_id == "None" - assert result[0].title == "Minimal Text" - assert result[0].categories is None - assert result[0].views == 0 - assert result[0].source_link is None - assert result[0].ranking is None - assert result[0].license is None - - -@pytest.mark.asyncio -async def test_get_text_by_pecha_text_ids_service_empty_request(): - """Test get_text_by_pecha_text_ids_service with empty pecha_text_ids list""" - request = TextsByPechaTextIdsRequest(pecha_text_ids=[]) - - with patch("pecha_api.texts.texts_service.get_texts_by_pecha_text_ids", new_callable=AsyncMock, return_value=[]): - result = await get_text_by_pecha_text_ids_service(texts_by_pecha_text_ids_request=request) - - assert result is not None - assert len(result) == 0 - assert result == [] - - -@pytest.mark.asyncio -async def test_get_text_by_pecha_text_ids_service_multiple_texts_various_types(): - """Test get_text_by_pecha_text_ids_service with texts of various types""" - class MockText: - def __init__(self, text_id, pecha_text_id, title, text_type): - self.id = text_id - self.pecha_text_id = pecha_text_id - self.title = title - self.language = "bo" - self.group_id = "group_id_1" - self.type = text_type - self.is_published = True - self.created_date = "2025-01-01T00:00:00" - self.updated_date = "2025-01-01T00:00:00" - self.published_date = "2025-01-01T00:00:00" - self.published_by = "user_1" - self.categories = [] - self.views = 10 - self.source_link = "https://source.com" - self.ranking = 1 - self.license = "CC0" - - mock_texts = [ - MockText("text_id_1", "pecha_1", "Root Text", "root_text"), - MockText("text_id_2", "pecha_2", "Version Text", "version"), - MockText("text_id_3", "pecha_3", "Commentary Text", "commentary") - ] - - request = TextsByPechaTextIdsRequest(pecha_text_ids=["pecha_1", "pecha_2", "pecha_3"]) - - with patch("pecha_api.texts.texts_service.get_texts_by_pecha_text_ids", new_callable=AsyncMock, return_value=mock_texts): - result = await get_text_by_pecha_text_ids_service(texts_by_pecha_text_ids_request=request) - - assert result is not None - assert len(result) == 3 - assert result[0].type == "root_text" - assert result[1].type == "version" - assert result[2].type == "commentary" - assert result[0].title == "Root Text" - assert result[1].title == "Version Text" - assert result[2].title == "Commentary Text" - - -@pytest.mark.asyncio -async def test_get_titles_and_ids_by_query_success(): - """Test get_titles_and_ids_by_query returns title search results""" - mock_response_data = [ - {"title": {"en": "A Title"}, "language": "en"}, - {"title": {"bo": "མཚན"}, "language": "bo"}, - {"title": {"en": "A Title"}, "language": "en"} - ] - - mock_http_response = Mock() - mock_http_response.json.return_value = mock_response_data - mock_http_response.raise_for_status = Mock() - - mock_client = AsyncMock() - mock_client.get = AsyncMock(return_value=mock_http_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - - class MockText: - def __init__(self, text_id: str, title: str): - self.id = text_id - self.title = title - - mock_texts = [ - MockText(text_id="id_1", title="A Title"), - MockText(text_id="id_2", title="མཚན") - ] - - with patch("pecha_api.texts.texts_service.EXTERNAL_TITLE_SEARCH_API_URL", "https://external.example"), \ - patch("httpx.AsyncClient", return_value=mock_client), \ - patch("pecha_api.texts.texts_service.get_texts_by_titles", new_callable=AsyncMock, return_value=mock_texts) as mock_get_texts: - result = await get_titles_and_ids_by_query( - title="Test", - author=None, - limit=20, - offset=0 - ) - - assert len(result) == 2 - assert result[0].id == "id_1" - assert result[0].title == "A Title" - assert result[1].id == "id_2" - assert result[1].title == "མཚན" - - call_args = mock_client.get.call_args - assert call_args.kwargs["params"]["title"] == "Test" - assert call_args.kwargs["params"]["limit"] == 20 - assert call_args.kwargs["params"]["offset"] == 0 - - mock_get_texts.assert_awaited_once() - called_titles = mock_get_texts.call_args.kwargs["titles"] - assert "A Title" in called_titles - assert "མཚན" in called_titles - - -def test_extract_title_for_language_variants(): - """Test extract_title_for_language with dict, string, and other payloads""" - from pecha_api.texts.texts_service import extract_title_for_language - - assert extract_title_for_language({"bo": "མཚན", "en": "Title"}, "bo") == "མཚན" - assert extract_title_for_language({"en": " ", "zh": "Title"}, "fr") == "Title" - assert extract_title_for_language(" A Title ", "bo") == "A Title" - assert extract_title_for_language(12345, "bo") is None - - -@pytest.mark.asyncio -async def test_get_text_by_text_id_or_collection_cache_hit(): - """Test get_text_by_text_id_or_collection returns cached data""" - text_id = "text_id_1" - cached_text = TextDTO( - id=text_id, - title="Cached Title", - language="bo", - group_id="group_id_1", - type="root_text", - is_published=True, - created_date="2025-03-21 09:40:34.025024", - updated_date="2025-03-21 09:40:34.025035", - published_date="2025-03-21 09:40:34.025038", - published_by="pecha", - categories=[], - views=0 - ) - - with patch("pecha_api.texts.texts_service.get_text_by_text_id_or_collection_cache", new_callable=AsyncMock, return_value=cached_text) as mock_get_cache, \ - patch("pecha_api.texts.texts_service.set_text_by_text_id_or_collection_cache", new_callable=AsyncMock) as mock_set_cache, \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock) as mock_get_detail: - result = await get_text_by_text_id_or_collection(text_id=text_id, collection_id=None) - - assert result == cached_text - mock_get_cache.assert_awaited_once() - mock_set_cache.assert_not_awaited() - mock_get_detail.assert_not_awaited() - - -@pytest.mark.asyncio -async def test_get_titles_and_ids_by_query_requires_title_or_author(): - """Test get_titles_and_ids_by_query requires title or author""" - with pytest.raises(HTTPException) as exc_info: - await get_titles_and_ids_by_query(title=None, author=None, limit=10, offset=0) - - assert exc_info.value.status_code == 400 - - -@pytest.mark.asyncio -async def test_get_titles_and_ids_by_query_missing_external_url(): - """Test get_titles_and_ids_by_query raises when API URL is missing""" - with patch("pecha_api.texts.texts_service.EXTERNAL_TITLE_SEARCH_API_URL", None): - with pytest.raises(HTTPException) as exc_info: - await get_titles_and_ids_by_query(title="Test", author=None, limit=10, offset=0) - - assert exc_info.value.status_code == 500 - - -@pytest.mark.asyncio -async def test_get_titles_and_ids_by_query_author_param_and_empty_titles(): - """Test get_titles_and_ids_by_query with author-only and no valid titles""" - mock_response_data = [ - {"title": {"en": " "}}, - "not-a-dict" - ] - - mock_http_response = Mock() - mock_http_response.json.return_value = mock_response_data - mock_http_response.raise_for_status = Mock() - - mock_client = AsyncMock() - mock_client.get = AsyncMock(return_value=mock_http_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - - with patch("pecha_api.texts.texts_service.EXTERNAL_TITLE_SEARCH_API_URL", "https://external.example"), \ - patch("httpx.AsyncClient", return_value=mock_client), \ - patch("pecha_api.texts.texts_service.get_texts_by_titles", new_callable=AsyncMock) as mock_get_texts: - result = await get_titles_and_ids_by_query( - title=None, - author="Author", - limit=20, - offset=0 - ) - - assert result == [] - call_args = mock_client.get.call_args - assert call_args.kwargs["params"]["author"] == "Author" - assert "title" not in call_args.kwargs["params"] - mock_get_texts.assert_not_awaited() - - -@pytest.mark.asyncio -async def test_get_titles_and_ids_by_query_http_status_error(): - """Test get_titles_and_ids_by_query handles HTTPStatusError""" - mock_request = httpx.Request("GET", "https://external.example/v2/texts") - mock_response = httpx.Response(500, request=mock_request) - - mock_http_response = Mock() - mock_http_response.raise_for_status.side_effect = httpx.HTTPStatusError( - "Server error", - request=mock_request, - response=mock_response - ) - - mock_client = AsyncMock() - mock_client.get = AsyncMock(return_value=mock_http_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - - with patch("pecha_api.texts.texts_service.EXTERNAL_TITLE_SEARCH_API_URL", "https://external.example"), \ - patch("httpx.AsyncClient", return_value=mock_client), \ - patch("pecha_api.texts.texts_service.handle_http_status_error", side_effect=HTTPException(status_code=502, detail="Bad Gateway")) as mock_handle: - with pytest.raises(HTTPException) as exc_info: - await get_titles_and_ids_by_query(title="Test", author=None, limit=10, offset=0) - - assert exc_info.value.status_code == 502 - mock_handle.assert_called_once() - - -@pytest.mark.asyncio -async def test_get_titles_and_ids_by_query_request_error(): - """Test get_titles_and_ids_by_query handles RequestError""" - mock_request = httpx.Request("GET", "https://external.example/v2/texts") - mock_client = AsyncMock() - mock_client.get = AsyncMock(side_effect=httpx.RequestError("Network error", request=mock_request)) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - - with patch("pecha_api.texts.texts_service.EXTERNAL_TITLE_SEARCH_API_URL", "https://external.example"), \ - patch("httpx.AsyncClient", return_value=mock_client), \ - patch("pecha_api.texts.texts_service.handle_request_error", side_effect=HTTPException(status_code=503, detail="Service unavailable")) as mock_handle: - with pytest.raises(HTTPException) as exc_info: - await get_titles_and_ids_by_query(title="Test", author=None, limit=10, offset=0) - - assert exc_info.value.status_code == 503 - mock_handle.assert_called_once() - - -@pytest.mark.asyncio -async def test_get_texts_by_collection_id_skip_duplicate_and_limit(): - """Test _get_texts_by_collection_id skip, duplicate, and limit handling""" - from pecha_api.texts.texts_service import _get_texts_by_collection_id - - class MockText: - def __init__(self, text_id: str, group_id: str, language: str): - self.id = text_id - self.pecha_text_id = f"pecha_{text_id}" - self.title = f"title_{text_id}" - self.language = language - self.group_id = group_id - self.type = "root_text" - self.is_published = True - self.created_date = "2025-01-01T00:00:00" - self.updated_date = "2025-01-01T00:00:00" - self.published_date = "2025-01-01T00:00:00" - self.published_by = "user_1" - - texts = [ - MockText("1", "g1", "bo"), - MockText("2", "g1", "bo"), - MockText("3", "g2", "bo") - ] - - with patch("pecha_api.texts.texts_service.get_all_texts_by_collection", new_callable=AsyncMock, return_value=texts), \ - patch("pecha_api.texts.texts_service.TextUtils.get_language_priority", return_value=0): - result, total_unique_group_ids = await _get_texts_by_collection_id( - collection_id="collection_1", - language="bo", - skip=0, - limit=10 - ) - - assert total_unique_group_ids == 2 - assert len(result) == 2 - assert result[0].group_id == "g1" - assert result[1].group_id == "g2" - - -@pytest.mark.asyncio -async def test_get_texts_by_collection_id_skip_first_group(): - """Test _get_texts_by_collection_id skip branch""" - from pecha_api.texts.texts_service import _get_texts_by_collection_id - - class MockText: - def __init__(self, text_id: str, group_id: str): - self.id = text_id - self.pecha_text_id = f"pecha_{text_id}" - self.title = f"title_{text_id}" - self.language = "bo" - self.group_id = group_id - self.type = "root_text" - self.is_published = True - self.created_date = "2025-01-01T00:00:00" - self.updated_date = "2025-01-01T00:00:00" - self.published_date = "2025-01-01T00:00:00" - self.published_by = "user_1" - - texts = [ - MockText("1", "g1"), - MockText("2", "g2") - ] - - with patch("pecha_api.texts.texts_service.get_all_texts_by_collection", new_callable=AsyncMock, return_value=texts), \ - patch("pecha_api.texts.texts_service.TextUtils.get_language_priority", return_value=0): - result, _ = await _get_texts_by_collection_id( - collection_id="collection_1", - language="bo", - skip=1, - limit=10 - ) - - assert len(result) == 1 - assert result[0].group_id == "g2" - - -@pytest.mark.asyncio -async def test_get_texts_by_collection_id_breaks_on_limit(): - """Test _get_texts_by_collection_id stops at limit""" - from pecha_api.texts.texts_service import _get_texts_by_collection_id - - class MockText: - def __init__(self, text_id: str, group_id: str): - self.id = text_id - self.pecha_text_id = f"pecha_{text_id}" - self.title = f"title_{text_id}" - self.language = "bo" - self.group_id = group_id - self.type = "root_text" - self.is_published = True - self.created_date = "2025-01-01T00:00:00" - self.updated_date = "2025-01-01T00:00:00" - self.published_date = "2025-01-01T00:00:00" - self.published_by = "user_1" - - texts = [ - MockText("1", "g1"), - MockText("2", "g2") - ] - - with patch("pecha_api.texts.texts_service.get_all_texts_by_collection", new_callable=AsyncMock, return_value=texts), \ - patch("pecha_api.texts.texts_service.TextUtils.get_language_priority", return_value=0): - result, _ = await _get_texts_by_collection_id( - collection_id="collection_1", - language="bo", - skip=0, - limit=1 - ) - - assert len(result) == 1 - assert result[0].group_id == "g1" - - -def test_get_first_segment_and_table_of_content_nested_sections(): - """Test _get_first_segment_and_table_of_content_ finds nested segment""" - from pecha_api.texts.texts_service import _get_first_segment_and_table_of_content_ - - table_of_contents = [ - TableOfContent( - id="toc_1", - text_id="text_id", - type=TableOfContentType.TEXT, - sections=[ - Section( - id="section_1", - title="Section 1", - section_number=1, - segments=[], - sections=[ - Section( - id="section_2", - title="Section 2", - section_number=2, - segments=[TextSegment(segment_id="seg_2", segment_number=1)], - sections=[] - ) - ] - ) - ] - ) - ] - - segment_id, table_of_content = _get_first_segment_and_table_of_content_( - table_of_contents=table_of_contents - ) - - assert segment_id == "seg_2" - assert table_of_content is not None - - -@pytest.mark.asyncio -async def test_get_commentaries_by_text_id_returns_not_found_when_false(): - """Test get_commentaries_by_text_id raises 404 when text is invalid""" - text_id = "missing_text_id" - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=False): - with pytest.raises(HTTPException) as exc_info: - await get_commentaries_by_text_id(text_id=text_id, skip=0, limit=10) - - assert exc_info.value.status_code == 404 - assert exc_info.value.detail == ErrorConstants.TEXT_NOT_FOUND_MESSAGE - - -# ============================================================================ -# get_text_languages Service Tests -# ============================================================================ - -@pytest.mark.asyncio -async def test_get_text_languages_success(): - """Test get_text_languages returns available languages with version counts""" - text_id = "123e4567-e89b-12d3-a456-426614174000" - group_id = "group_id_1" - - mock_text_detail = TextDTO( - id=text_id, - title="Test Text", - language="bo", - group_id=group_id, - type="version", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="test_user", - categories=[], - views=0 - ) - - mock_texts = [ - MagicMock(language="bo"), - MagicMock(language="bo"), - MagicMock(language="en"), - MagicMock(language="zh") + published_by="pecha", + categories=[], + views=0 + ) ] - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_all_texts_by_group_id", new_callable=AsyncMock, return_value=mock_texts): + # Mock filters for each group + def mock_filter_side_effect(texts, language): + if texts[0].group_id == group_id_1: + return {"root_text": texts[0], "versions": [texts[1]]} + elif texts[0].group_id == group_id_2: + return {"root_text": texts[0], "versions": [texts[1]]} + return {"root_text": None, "versions": texts} + + with patch("pecha_api.texts.texts_service.get_all_recitation_texts_by_collection", new_callable=AsyncMock) as mock_get_all_texts, \ + patch("pecha_api.texts.texts_service.TextUtils.filter_text_base_on_group_id_type_and_language_preference", new_callable=AsyncMock) as mock_filter: + + mock_get_all_texts.return_value = mock_texts + mock_filter.side_effect = mock_filter_side_effect - response = await get_text_languages(text_id=text_id) + result = await get_root_text_by_collection_id(collection_id=collection_id, language=language) - assert response is not None - assert isinstance(response, LanguageResponse) - assert response.text_id == text_id - assert response.title == "Test Text" - assert len(response.available_languages) == 3 + mock_get_all_texts.assert_called_once_with(collection_id=collection_id, language=language) - lang_dict = {lang.language: lang.version_count for lang in response.available_languages} - assert lang_dict["bo"] == 2 - assert lang_dict["en"] == 1 - assert lang_dict["zh"] == 1 - + assert result is not None + assert isinstance(result, RecitationsResponse) + assert len(result.recitations) == 2 + assert isinstance(result.recitations[0], RecitationDTO) + assert isinstance(result.recitations[1], RecitationDTO) + # Check that we got both root texts + text_ids = {str(rec.text_id) for rec in result.recitations} + assert text_id_1 in text_ids + assert text_id_3 in text_ids @pytest.mark.asyncio -async def test_get_text_languages_text_not_found(): - """Test get_text_languages raises 404 when text doesn't exist""" - text_id = "non-existent-id" +async def test_get_table_of_content_by_type_text_type(): + """Test get_table_of_content_by_type with TEXT type""" + text_id = "text_id_1" - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=False): - with pytest.raises(HTTPException) as exc_info: - await get_text_languages(text_id=text_id) + incoming_toc = TableOfContent( + id="toc_id_1", + text_id=text_id, + type=TableOfContentType.TEXT, + sections=[ + Section( + id="section_id_1", + title="section_1", + section_number=1, + parent_id="parent_id_1", + segments=[ + TextSegment(segment_id="pseg_1", segment_number=1) + ], + sections=[], + created_date="2025-03-16 04:40:54.757652", + updated_date="2025-03-16 04:40:54.757652", + published_date="2025-03-16 04:40:54.757652" + ) + ] + ) + + expected_toc = TableOfContent( + text_id=text_id, + type=TableOfContentType.TEXT, + sections=[ + Section( + id="section_id_1", + title="section_1", + section_number=1, + segments=[TextSegment(segment_id="seg_id_1", segment_number=1)] + ) + ] + ) + + with patch("pecha_api.texts.texts_service.get_segments_by_text_id", new_callable=AsyncMock) as mock_get_segments: + mock_get_segments.return_value = [type("Seg", (), {"id": "seg_id_1", "pecha_segment_id": "pseg_1"})()] - assert exc_info.value.status_code == 404 - assert exc_info.value.detail == ErrorConstants.TEXT_NOT_FOUND_MESSAGE - + result = await get_table_of_content_by_type(table_of_content=incoming_toc) + + assert result is not None + assert isinstance(result, TableOfContent) + assert result.text_id == text_id + assert result.type == TableOfContentType.TEXT + assert len(result.sections) == 1 + assert result.sections[0].segments[0].segment_id == "seg_id_1" @pytest.mark.asyncio -async def test_get_text_languages_empty_languages(): - """Test get_text_languages returns empty list when no texts have languages""" - text_id = "123e4567-e89b-12d3-a456-426614174000" - group_id = "group_id_1" +async def test_get_table_of_content_by_type_sheet_type(): + """Test get_table_of_content_by_type with SHEET type""" + text_id = "text_id_1" - mock_text_detail = TextDTO( - id=text_id, - title="Test Text", - language=None, - group_id=group_id, - type="version", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="test_user", - categories=[], - views=0 + incoming_toc = TableOfContent( + id="toc_id_1", + text_id=text_id, + type=TableOfContentType.SHEET, + sections=[ + Section( + id="section_id_1", + title="section_1", + section_number=1, + parent_id="parent_id_1", + segments=[ + TextSegment(segment_id="seg_1", segment_number=1) + ], + sections=[], + created_date="2025-03-16 04:40:54.757652", + updated_date="2025-03-16 04:40:54.757652", + published_date="2025-03-16 04:40:54.757652" + ) + ] ) - mock_texts = [ - MagicMock(language=None), - MagicMock(language=None) - ] + result = await get_table_of_content_by_type(table_of_content=incoming_toc) - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_all_texts_by_group_id", new_callable=AsyncMock, return_value=mock_texts): - - response = await get_text_languages(text_id=text_id) - - assert response is not None - assert isinstance(response, LanguageResponse) - assert response.available_languages == [] + assert result is not None + assert isinstance(result, TableOfContent) + assert result.text_id == text_id + assert result.type == TableOfContentType.SHEET + assert result.sections == incoming_toc.sections + @pytest.mark.asyncio -async def test_get_text_languages_single_language(): - """Test get_text_languages with single language multiple versions""" - text_id = "123e4567-e89b-12d3-a456-426614174000" - group_id = "group_id_1" - - mock_text_detail = TextDTO( - id=text_id, - title="Tibetan Only Text", +async def test_update_text_details_cache_update_fails(): + """Test update_text_details when cache update fails""" + mock_text_details = TextDTO( + id="text_id_1", + title="text_title", language="bo", - group_id=group_id, + group_id="group_id_1", type="version", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="test_user", + is_published=False, + created_date="created_date", + updated_date="updated_date", + published_date="published_date", + published_by="published_by", categories=[], views=0 ) - mock_texts = [ - MagicMock(language="bo"), - MagicMock(language="bo"), - MagicMock(language="bo"), - MagicMock(language="bo"), - MagicMock(language="bo") - ] - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_all_texts_by_group_id", new_callable=AsyncMock, return_value=mock_texts): + patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_details), \ + patch("pecha_api.texts.texts_service.update_text_details_by_id", new_callable=AsyncMock, return_value=mock_text_details), \ + patch("pecha_api.texts.texts_service.update_text_details_cache", new_callable=AsyncMock, side_effect=Exception("Cache error")), \ + patch("pecha_api.texts.texts_service.invalidate_text_cache_on_update", new_callable=AsyncMock, return_value=None) as mock_invalidate: - response = await get_text_languages(text_id=text_id) + response = await update_text_details(text_id="text_id_1", update_text_request=UpdateTextRequest(title="updated_title", is_published=True)) assert response is not None - assert len(response.available_languages) == 1 - assert response.available_languages[0].language == "bo" - assert response.available_languages[0].language_code == "bo" - assert response.available_languages[0].version_count == 5 - - -# ============================================================================ -# get_language_versions Service Tests -# ============================================================================ + # Verify that invalidate was called as fallback + mock_invalidate.assert_called_once_with(text_id="text_id_1") @pytest.mark.asyncio -async def test_get_language_versions_success(): - """Test get_language_versions returns versions for a specific language""" +async def test_update_text_details_with_cache_invalidation(): + """Test update_text_details updates text successfully""" text_id = "123e4567-e89b-12d3-a456-426614174000" - language = "bo" - group_id = "group_id_1" + update_request = UpdateTextRequest( + title="Updated Title", + language="bo" + ) - mock_text_detail = TextDTO( + updated_text = TextDTO( id=text_id, - title="Test Text", + title="Updated Title", language="bo", - group_id=group_id, + group_id="group_id_1", type="version", is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", + created_date="2025-03-21 09:40:34.025024", + updated_date="2025-03-21 09:40:34.025035", + published_date="2025-03-21 09:40:34.025038", published_by="pecha", categories=[], views=0 ) + with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ + patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=updated_text), \ + patch("pecha_api.texts.texts_service.update_text_details_by_id", new_callable=AsyncMock, return_value=updated_text) as mock_update, \ + patch("pecha_api.texts.texts_service.invalidate_text_cache_on_update", new_callable=AsyncMock): + + result = await update_text_details(text_id=text_id, update_text_request=update_request) + + assert result is not None + assert result.title == "Updated Title" + mock_update.assert_called_once_with(text_id=text_id, update_text_request=update_request) + +# Tests for new private functions +def test_group_texts_by_group_id_with_language_sorting(): + """Test _group_texts_by_group_id groups texts and sorts by language preference""" + from pecha_api.texts.texts_service import _group_texts_by_group_id + + # Create mock texts with different group_ids and languages mock_texts = [ TextDTO( - id=text_id, - title="Version 1", + id="text_id_1", + title="Text 1 Bo", language="bo", - group_id=group_id, + group_id="group_1", + type="version", + is_published=True, + created_date="2025-03-21 09:40:34.025024", + updated_date="2025-03-21 09:40:34.025035", + published_date="2025-03-21 09:40:34.025038", + published_by="pecha", + categories=[], + views=0 + ), + TextDTO( + id="text_id_2", + title="Text 2 En", + language="en", + group_id="group_1", + type="version", + is_published=True, + created_date="2025-03-21 09:40:34.025024", + updated_date="2025-03-21 09:40:34.025035", + published_date="2025-03-21 09:40:34.025038", + published_by="pecha", + categories=[], + views=0 + ), + TextDTO( + id="text_id_3", + title="Text 3 Zh", + language="zh", + group_id="group_1", type="version", is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", + created_date="2025-03-21 09:40:34.025024", + updated_date="2025-03-21 09:40:34.025035", + published_date="2025-03-21 09:40:34.025038", published_by="pecha", categories=[], - views=0, - source_link="https://source-1.com", - ranking=1, - license="CC0" + views=0 ), TextDTO( - id="text-2-uuid", - title="Version 2", + id="text_id_4", + title="Text 4 Bo Group2", language="bo", - group_id=group_id, + group_id="group_2", + type="version", + is_published=True, + created_date="2025-03-21 09:40:34.025024", + updated_date="2025-03-21 09:40:34.025035", + published_date="2025-03-21 09:40:34.025038", + published_by="pecha", + categories=[], + views=0 + ) + ] + + # Test with 'en' as preferred language + result = _group_texts_by_group_id(texts=mock_texts, language="en") + + # Verify grouping + assert len(result) == 2 + assert "group_1" in result + assert "group_2" in result + assert len(result["group_1"]) == 3 + assert len(result["group_2"]) == 1 + + # Verify sorting - 'en' should be first for group_1 + assert result["group_1"][0].language == "en" + assert result["group_1"][1].language == "bo" + assert result["group_1"][2].language == "zh" + + +def test_group_texts_by_group_id_with_bo_preference(): + """Test _group_texts_by_group_id with Tibetan (bo) language preference""" + from pecha_api.texts.texts_service import _group_texts_by_group_id + + mock_texts = [ + TextDTO( + id="text_id_1", + title="Text En", + language="en", + group_id="group_1", type="version", is_published=True, - created_date="2025-01-02T00:00:00", - updated_date="2025-01-02T00:00:00", - published_date="2025-01-02T00:00:00", + created_date="2025-03-21 09:40:34.025024", + updated_date="2025-03-21 09:40:34.025035", + published_date="2025-03-21 09:40:34.025038", published_by="pecha", categories=[], - views=0, - source_link="https://source-2.com", - ranking=2, - license="CC BY" + views=0 ), TextDTO( - id="text-3-uuid", - title="English Version", - language="en", - group_id=group_id, - type="translation", + id="text_id_2", + title="Text Bo", + language="bo", + group_id="group_1", + type="version", is_published=True, - created_date="2025-01-03T00:00:00", - updated_date="2025-01-03T00:00:00", - published_date="2025-01-03T00:00:00", + created_date="2025-03-21 09:40:34.025024", + updated_date="2025-03-21 09:40:34.025035", + published_date="2025-03-21 09:40:34.025038", published_by="pecha", categories=[], views=0 ) ] - mock_table_of_content = TableOfContent( - id="toc-1", - text_id=text_id, - type=TableOfContentType.TEXT, - sections=[] - ) + result = _group_texts_by_group_id(texts=mock_texts, language="bo") - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_all_texts_by_group_id", new_callable=AsyncMock, return_value=mock_texts), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=[mock_table_of_content]): - - response = await get_language_versions(text_id=text_id, language=language) - - assert response is not None - assert isinstance(response, VersionsResponse) - assert response.text_id == text_id - assert response.language == language - assert len(response.available_versions) == 2 - assert all(v.language == "bo" for v in response.available_versions) - - selected_versions = [v for v in response.available_versions if v.is_selected] - assert len(selected_versions) == 1 - assert selected_versions[0].id == text_id + # Verify 'bo' is first + assert result["group_1"][0].language == "bo" + assert result["group_1"][1].language == "en" -@pytest.mark.asyncio -async def test_get_language_versions_text_not_found(): - """Test get_language_versions raises 404 when text doesn't exist""" - text_id = "non-existent-id" - language = "bo" +def test_get_language_priority_via_textutils(): + """Test language priority through TextUtils (used by service layer)""" + from pecha_api.texts.texts_utils import TextUtils - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=False): - with pytest.raises(HTTPException) as exc_info: - await get_language_versions(text_id=text_id, language=language) - - assert exc_info.value.status_code == 404 - assert exc_info.value.detail == ErrorConstants.TEXT_NOT_FOUND_MESSAGE + # Test with 'bo' preference + assert TextUtils.get_language_priority("bo", "bo") == 0 + assert TextUtils.get_language_priority("en", "bo") == 1 + assert TextUtils.get_language_priority("zh", "bo") == 2 + assert TextUtils.get_language_priority("unknown", "bo") == 999 + + # Test with 'en' preference + assert TextUtils.get_language_priority("en", "en") == 0 + assert TextUtils.get_language_priority("bo", "en") == 1 + assert TextUtils.get_language_priority("zh", "en") == 2 + assert TextUtils.get_language_priority("unknown", "en") == 999 + + # Test with 'zh' preference + assert TextUtils.get_language_priority("zh", "zh") == 0 + assert TextUtils.get_language_priority("en", "zh") == 1 + assert TextUtils.get_language_priority("bo", "zh") == 2 -@pytest.mark.asyncio -async def test_get_language_versions_empty_for_language(): - """Test get_language_versions returns empty list when no versions for requested language""" - text_id = "123e4567-e89b-12d3-a456-426614174000" - language = "zh" - group_id = "group_id_1" +def test_get_language_priority_with_none_via_textutils(): + """Test TextUtils.get_language_priority handles None text_language""" + from pecha_api.texts.texts_utils import TextUtils - mock_text_detail = TextDTO( - id=text_id, - title="Test Text", - language="bo", - group_id=group_id, - type="version", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="pecha", - categories=[], - views=0 - ) + # None language should get default priority + assert TextUtils.get_language_priority(None, "bo") == 999 + assert TextUtils.get_language_priority(None, "en") == 999 + + +def test_group_texts_by_group_id_empty_texts(): + """Test _group_texts_by_group_id with empty text list""" + from pecha_api.texts.texts_service import _group_texts_by_group_id - mock_texts = [ - TextDTO( - id=text_id, - title="Tibetan Version", - language="bo", - group_id=group_id, - type="version", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="pecha", - categories=[], - views=0 - ), - TextDTO( - id="text-2-uuid", - title="English Version", - language="en", - group_id=group_id, - type="translation", - is_published=True, - created_date="2025-01-02T00:00:00", - updated_date="2025-01-02T00:00:00", - published_date="2025-01-02T00:00:00", - published_by="pecha", - categories=[], - views=0 - ) - ] + result = _group_texts_by_group_id(texts=[], language="bo") - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_all_texts_by_group_id", new_callable=AsyncMock, return_value=mock_texts): - - response = await get_language_versions(text_id=text_id, language=language) - - assert response is not None - assert isinstance(response, VersionsResponse) - assert response.text_id == text_id - assert response.language == language - assert len(response.available_versions) == 0 + assert result == {} -@pytest.mark.asyncio -async def test_get_language_versions_with_table_of_contents(): - """Test get_language_versions includes table_of_contents for each version""" - text_id = "123e4567-e89b-12d3-a456-426614174000" - language = "bo" - group_id = "group_id_1" - - mock_text_detail = TextDTO( - id=text_id, - title="Test Text", - language="bo", - group_id=group_id, - type="version", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="pecha", - categories=[], - views=0 - ) +def test_group_texts_by_group_id_single_group(): + """Test _group_texts_by_group_id with single group""" + from pecha_api.texts.texts_service import _group_texts_by_group_id mock_texts = [ TextDTO( - id=text_id, - title="Version 1", + id="text_id_1", + title="Text 1", language="bo", - group_id=group_id, + group_id="single_group", type="version", is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", + created_date="2025-03-21 09:40:34.025024", + updated_date="2025-03-21 09:40:34.025035", + published_date="2025-03-21 09:40:34.025038", published_by="pecha", categories=[], views=0 ) ] - mock_table_of_contents = [ - TableOfContent(id="toc-1", text_id=text_id, type=TableOfContentType.TEXT, sections=[]), - TableOfContent(id="toc-2", text_id=text_id, type=TableOfContentType.TEXT, sections=[]) - ] + result = _group_texts_by_group_id(texts=mock_texts, language="bo") - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_all_texts_by_group_id", new_callable=AsyncMock, return_value=mock_texts), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=mock_table_of_contents): - - response = await get_language_versions(text_id=text_id, language=language) - - assert response is not None - assert len(response.available_versions) == 1 - assert len(response.available_versions[0].table_of_contents) == 2 - assert "toc-1" in response.available_versions[0].table_of_contents - assert "toc-2" in response.available_versions[0].table_of_contents + assert len(result) == 1 + assert "single_group" in result + assert len(result["single_group"]) == 1 + assert result["single_group"][0].id == "text_id_1" -# ============================================================================ -# get_version_info Service Tests -# ============================================================================ @pytest.mark.asyncio -async def test_get_version_info_success(): - """Test get_version_info returns version details for a valid version_id""" - version_id = "123e4567-e89b-12d3-a456-426614174000" - group_id = "group_id_1" +async def test_replace_pecha_segment_id_with_segment_id_success(): + """Test replace_pecha_segment_id_with_segment_id converts pecha segment IDs to database segment IDs""" + text_id = "text_id_1" - mock_text_detail = TextDTO( - id=version_id, - title="Test Version", - language="bo", - group_id=group_id, - type="version", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="pecha", - categories=[], - views=0, - source_link="https://source.com", - ranking=1, - license="CC0" - ) + # Mock segments from database + class MockSegment: + def __init__(self, segment_id, pecha_segment_id): + self.id = segment_id + self.pecha_segment_id = pecha_segment_id - mock_table_of_contents = [ - TableOfContent(id="toc-1", text_id=version_id, type=TableOfContentType.TEXT, sections=[]), - TableOfContent(id="toc-2", text_id=version_id, type=TableOfContentType.TEXT, sections=[]) + mock_segments = [ + MockSegment("db_seg_1", "pecha_seg_1"), + MockSegment("db_seg_2", "pecha_seg_2") ] - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=mock_table_of_contents): - - response = await get_version_info(version_id=version_id) - - assert response is not None - assert isinstance(response, VersionDetail) - assert response.id == version_id - assert response.title == "Test Version" - assert response.language == "bo" - assert response.type == "version" - assert response.group_id == group_id - assert response.is_published == True - assert response.is_selected == True - assert len(response.table_of_contents) == 2 - assert "toc-1" in response.table_of_contents - assert "toc-2" in response.table_of_contents - assert response.source_link == "https://source.com" - assert response.ranking == 1 - assert response.license == "CC0" - - -@pytest.mark.asyncio -async def test_get_version_info_text_not_found(): - """Test get_version_info raises 404 when version doesn't exist""" - version_id = "non-existent-id" - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=False): - with pytest.raises(HTTPException) as exc_info: - await get_version_info(version_id=version_id) - - assert exc_info.value.status_code == 404 - assert exc_info.value.detail == ErrorConstants.TEXT_NOT_FOUND_MESSAGE - - -@pytest.mark.asyncio -async def test_get_version_info_empty_table_of_contents(): - """Test get_version_info returns empty table_of_contents when no TOC exists""" - version_id = "123e4567-e89b-12d3-a456-426614174000" - group_id = "group_id_1" - - mock_text_detail = TextDTO( - id=version_id, - title="Version without TOC", - language="bo", - group_id=group_id, - type="version", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="pecha", - categories=[], - views=0 + incoming_toc = TableOfContent( + text_id=text_id, + type=TableOfContentType.TEXT, + sections=[ + Section( + id="section_1", + title="Section 1", + section_number=1, + segments=[ + TextSegment(segment_id="pecha_seg_1", segment_number=1), + TextSegment(segment_id="pecha_seg_2", segment_number=2) + ] + ) + ] ) - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=[]): - - response = await get_version_info(version_id=version_id) + with patch("pecha_api.texts.texts_service.get_segments_by_text_id", new_callable=AsyncMock, return_value=mock_segments): + result = await replace_pecha_segment_id_with_segment_id(table_of_content=incoming_toc) - assert response is not None - assert isinstance(response, VersionDetail) - assert response.id == version_id - assert response.table_of_contents == [] + assert result is not None + assert isinstance(result, TableOfContent) + assert result.text_id == text_id + assert result.type == TableOfContentType.TEXT + assert len(result.sections) == 1 + assert len(result.sections[0].segments) == 2 + # Verify segment IDs were replaced + assert result.sections[0].segments[0].segment_id == "db_seg_1" + assert result.sections[0].segments[1].segment_id == "db_seg_2" @pytest.mark.asyncio -async def test_get_version_info_with_optional_fields_none(): - """Test get_version_info handles optional fields as None""" - version_id = "123e4567-e89b-12d3-a456-426614174000" - group_id = "group_id_1" +async def test_replace_pecha_segment_id_with_segment_id_multiple_sections(): + """Test replace_pecha_segment_id_with_segment_id with multiple sections""" + text_id = "text_id_1" - mock_text_detail = TextDTO( - id=version_id, - title="Version with None fields", - language="bo", - group_id=group_id, - type="version", - is_published=False, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="pecha", - categories=[], - views=0, - source_link=None, - ranking=None, - license=None - ) + class MockSegment: + def __init__(self, segment_id, pecha_segment_id): + self.id = segment_id + self.pecha_segment_id = pecha_segment_id - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=[]): - - response = await get_version_info(version_id=version_id) - - assert response is not None - assert isinstance(response, VersionDetail) - assert response.source_link is None - assert response.ranking is None - assert response.license is None - assert response.parent_id is None - assert response.priority is None - assert response.is_published == False - - -@pytest.mark.asyncio -async def test_get_version_info_translation_type(): - """Test get_version_info works with translation type versions""" - version_id = "123e4567-e89b-12d3-a456-426614174000" - group_id = "group_id_1" + mock_segments = [ + MockSegment("db_seg_1", "pecha_seg_1"), + MockSegment("db_seg_2", "pecha_seg_2"), + MockSegment("db_seg_3", "pecha_seg_3") + ] - mock_text_detail = TextDTO( - id=version_id, - title="English Translation", - language="en", - group_id=group_id, - type="translation", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="translator", - categories=["category-1"], - views=100, - source_link="https://translation.com", - ranking=2, - license="CC BY" + incoming_toc = TableOfContent( + text_id=text_id, + type=TableOfContentType.TEXT, + sections=[ + Section( + id="section_1", + title="Section 1", + section_number=1, + segments=[ + TextSegment(segment_id="pecha_seg_1", segment_number=1) + ] + ), + Section( + id="section_2", + title="Section 2", + section_number=2, + segments=[ + TextSegment(segment_id="pecha_seg_2", segment_number=1), + TextSegment(segment_id="pecha_seg_3", segment_number=2) + ] + ) + ] ) - mock_table_of_contents = [ - TableOfContent(id="toc-1", text_id=version_id, type=TableOfContentType.TEXT, sections=[]) - ] - - with patch("pecha_api.texts.texts_service.TextUtils.validate_text_exists", new_callable=AsyncMock, return_value=True), \ - patch("pecha_api.texts.texts_service.TextUtils.get_text_detail_by_id", new_callable=AsyncMock, return_value=mock_text_detail), \ - patch("pecha_api.texts.texts_service.get_contents_by_id", new_callable=AsyncMock, return_value=mock_table_of_contents): - - response = await get_version_info(version_id=version_id) + with patch("pecha_api.texts.texts_service.get_segments_by_text_id", new_callable=AsyncMock, return_value=mock_segments): + result = await replace_pecha_segment_id_with_segment_id(table_of_content=incoming_toc) - assert response is not None - assert isinstance(response, VersionDetail) - assert response.type == "translation" - assert response.language == "en" - assert response.title == "English Translation" - assert len(response.table_of_contents) == 1 \ No newline at end of file + assert result is not None + assert len(result.sections) == 2 + assert result.sections[0].segments[0].segment_id == "db_seg_1" + assert result.sections[1].segments[0].segment_id == "db_seg_2" + assert result.sections[1].segments[1].segment_id == "db_seg_3" \ No newline at end of file diff --git a/tests/texts/test_texts_views.py b/tests/texts/test_texts_views.py deleted file mode 100644 index 99f0bb1d4..000000000 --- a/tests/texts/test_texts_views.py +++ /dev/null @@ -1,1640 +0,0 @@ -from unittest.mock import AsyncMock, patch - -import pytest -from fastapi import HTTPException, status -from fastapi.testclient import TestClient -from httpx import AsyncClient, ASGITransport - -from pecha_api.app import api -from pecha_api.texts.texts_response_models import ( - CreateTextRequest, - TableOfContentResponse, - TextDTO, - TextVersionResponse, - TextVersion, - TableOfContent, - TableOfContentType, - Section, - TextDetailsRequest, - TextsByPechaTextIdsRequest, - TitleSearchResult, - LanguageResponse, - AvailableLanguage, - VersionDetail, - VersionsResponse -) - -client = TestClient(api) - -# Test data -MOCK_TEXT_DTO = TextDTO( - id="123e4567-e89b-12d3-a456-426614174000", - pecha_text_id="test_pecha_id", - title="Test Text", - language="bo", - group_id="123e4567-e89b-12d3-a456-426614174000", - type="version", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="test_user", - categories=[], - views=0, - source_link="https://test-source.com", - ranking=1, - license="CC0" -) - -# Create a simple section for the table of contents -mock_section = Section( - id="123e4567-e89b-12d3-a456-426614174002", - title="Chapter 1", - section_number=1, - parent_id=None, - segments=[], - sections=[], - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00" -) - -# Create a table of content with the section -MOCK_TABLE_OF_CONTENT = TableOfContent( - id="123e4567-e89b-12d3-a456-426614174001", - text_id="123e4567-e89b-12d3-a456-426614174000", - type=TableOfContentType.TEXT, - sections=[mock_section] -) - - - -# Create response objects -# Create a proper DetailTableOfContentResponse -MOCK_TABLE_OF_CONTENT_RESPONSE = TableOfContentResponse( - text_detail=MOCK_TEXT_DTO, - contents=[MOCK_TABLE_OF_CONTENT] -) - -# Create a TextVersion instance for the response -MOCK_TEXT_VERSION = TextVersion( - id="123e4567-e89b-12d3-a456-426614174003", - title="Test Text Version", - parent_id=None, - priority=1, - language="bo", - type="version", - group_id="123e4567-e89b-12d3-a456-426614174004", - table_of_contents=[], - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="test_user" -) - -MOCK_TEXT_VERSION_RESPONSE = TextVersionResponse( - text=MOCK_TEXT_DTO, - versions=[MOCK_TEXT_VERSION] -) - -VALID_TOKEN = "valid_token_123" - -@pytest.mark.asyncio -async def test_get_text_by_text_id(mocker): - """Test GET /texts with text_id parameter""" - mock_get_text = mocker.patch( - 'pecha_api.texts.texts_views.get_text_by_text_id_or_collection', - new_callable=AsyncMock, - return_value=MOCK_TEXT_DTO - ) - - test_text_id = "123e4567-e89b-12d3-a456-426614174000" - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get(f"/texts?text_id={test_text_id}&language=bo&skip=0&limit=10") - - # Assertions - assert response.status_code == 200 - response_data = response.json() - assert isinstance(response_data, dict) - assert response_data["id"] == MOCK_TEXT_DTO.id - assert response_data["title"] == MOCK_TEXT_DTO.title - assert response_data["language"] == MOCK_TEXT_DTO.language - mock_get_text.assert_called_once_with( - text_id="123e4567-e89b-12d3-a456-426614174000", - collection_id=None, - language="bo", - skip=0, - limit=10 - ) - -@pytest.mark.asyncio -async def test_get_text_by_collection_id(mocker): - """Test GET /texts with collection_id parameter""" - # Mock the service function - mock_get_text = mocker.patch( - 'pecha_api.texts.texts_views.get_text_by_text_id_or_collection', - new_callable=AsyncMock, - return_value=MOCK_TEXT_DTO - ) - - # The collection ID that will be used in the request - test_collection_id = "123e4567-e89b-12d3-a456-426614174001" - - # Make the request - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get(f"/texts?collection_id={test_collection_id}&language=bo&skip=0&limit=10") - - # Assertions - assert response.status_code == 200 - response_data = response.json() - assert isinstance(response_data, dict) - assert response_data["id"] == MOCK_TEXT_DTO.id - assert response_data["title"] == MOCK_TEXT_DTO.title - assert response_data["language"] == MOCK_TEXT_DTO.language - # Remove the contents check as it's not part of the TextDTO model - mock_get_text.assert_called_once_with( - text_id=None, - collection_id=test_collection_id, - language="bo", - skip=0, - limit=10 - ) - -@pytest.mark.asyncio -async def test_create_text_success(mocker): - """Test POST /texts with valid data""" - # Mock the service function - mock_create_text = mocker.patch( - 'pecha_api.texts.texts_views.create_new_text', - new_callable=AsyncMock, - return_value=MOCK_TEXT_DTO - ) - - # Test data - match the CreateTextRequest model - # Note: The mock returns MOCK_TEXT_DTO which has title="Test Text" - create_data = { - "pecha_text_id": "test_pecha_id", - "title": "Test Text", # Match the mock data - "language": "bo", - "isPublished": True, - "group_id": "123e4567-e89b-12d3-a456-426614174000", - "type": "version", - "published_by": "test_user", - "categories": [], - "source_link": "https://test-source.com", - "ranking": 1, - "license": "CC0" - } - - # Make the request - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post( - "/texts", - json=create_data, - headers={"Authorization": f"Bearer {VALID_TOKEN}"} - ) - - # Assertions - 201 Created is the correct status code for resource creation - assert response.status_code == 201 - response_data = response.json() - assert response_data["title"] == create_data["title"] - assert response_data["language"] == create_data["language"] - assert response_data["type"] == create_data["type"] - assert response_data["is_published"] == create_data["isPublished"] - mock_create_text.assert_called_once() - call_args = mock_create_text.call_args[1] - assert call_args["token"] == VALID_TOKEN - assert isinstance(call_args["create_text_request"], CreateTextRequest) - -@pytest.mark.asyncio -async def test_get_versions_success(mocker): - """Test GET /texts/{text_id}/versions""" - # Mock the service function - mock_get_versions = mocker.patch( - 'pecha_api.texts.texts_views.get_text_versions_by_group_id', - new_callable=AsyncMock, - return_value={ - "text": MOCK_TEXT_DTO, - "versions": [MOCK_TEXT_VERSION.model_dump()] - } - ) - - # The text ID that will be used in the request - test_text_id = "123e4567-e89b-12d3-a456-426614174000" - - # Make the request - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get(f"/texts/{test_text_id}/versions?language=bo&skip=0&limit=10") - - # Assertions - assert response.status_code == 200 - response_data = response.json() - assert "text" in response_data - assert "versions" in response_data - assert isinstance(response_data["versions"], list) - if response_data["versions"]: # Check only if versions list is not empty - version = response_data["versions"][0] - assert "id" in version - assert "title" in version - assert "language" in version - mock_get_versions.assert_called_once_with( - text_id=test_text_id, - language="bo", - skip=0, - limit=10 - ) - -@pytest.mark.asyncio -async def test_get_contents_success(mocker): - """Test GET /texts/{text_id}/contents""" - # Mock the service function - mock_get_contents = mocker.patch( - 'pecha_api.texts.texts_views.get_table_of_contents_by_text_id', - new_callable=AsyncMock, - return_value=MOCK_TABLE_OF_CONTENT_RESPONSE - ) - - # Make the request - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/123e4567-e89b-12d3-a456-426614174000/contents") - - # Assertions - assert response.status_code == 200 - response_data = response.json() - assert "text_detail" in response_data - assert "contents" in response_data - assert isinstance(response_data["contents"], list) - assert len(response_data["contents"]) > 0 - mock_get_contents.assert_called_once_with( - text_id="123e4567-e89b-12d3-a456-426614174000", - language=None, - skip=0, - limit=10 - ) - - -@pytest.mark.asyncio -async def test_create_table_of_content_success(mocker): - """Test POST /texts/table-of-content""" - # Mock the service function - mock_create_toc = mocker.patch( - 'pecha_api.texts.texts_views.create_table_of_content', - new_callable=AsyncMock, - return_value=MOCK_TABLE_OF_CONTENT - ) - - # Test data - match the TableOfContent model - toc_data = { - "id": "123e4567-e89b-12d3-a456-426614174001", - "text_id": "123e4567-e89b-12d3-a456-426614174000", - "type": "text", - "sections": [ - { - "id": "123e4567-e89b-12d3-a456-426614174002", - "title": "Chapter 1", - "section_number": 1, - "parent_id": None, - "segments": [], - "sections": [], - "created_date": "2025-01-01T00:00:00", - "updated_date": "2025-01-01T00:00:00", - "published_date": "2025-01-01T00:00:00", - "published_by": "test_user" - } - ] - } - - # Make the request - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post( - "/texts/table-of-content", - json=toc_data, - headers={"Authorization": f"Bearer {VALID_TOKEN}"} - ) - - # Assertions - assert response.status_code == 200 - response_data = response.json() - assert "id" in response_data - assert "text_id" in response_data - assert "sections" in response_data - assert response_data["text_id"] == toc_data["text_id"] - mock_create_toc.assert_called_once() - call_args = mock_create_toc.call_args[1] - assert call_args["token"] == VALID_TOKEN - assert isinstance(call_args["table_of_content_request"], TableOfContent) - -# Error case tests - -@pytest.mark.asyncio -async def test_create_text_unauthorized(): - """Test POST /texts without authentication""" - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post("/texts", json={"title": "Test"}) - assert response.status_code == 403 - -@pytest.mark.asyncio -async def test_create_table_of_content_unauthorized(): - """Test POST /texts/table-of-content without authentication""" - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post("/texts/table-of-content", json={"text_id": "123e4567-e89b-12d3-a456-426614174000"}) - assert response.status_code == 403 - -@pytest.mark.asyncio -async def test_get_contents_not_found(mocker): - """Test GET /texts/{text_id}/contents with non-existent text""" - mocker.patch( - 'pecha_api.texts.texts_views.get_table_of_contents_by_text_id', - side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Text not found") - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/non_existent_id/contents") - - assert response.status_code == status.HTTP_404_NOT_FOUND - assert response.json()["detail"] == "Text not found" - -@pytest.mark.asyncio -async def test_get_commentaries_success(mocker): - """Test GET /texts/{text_id}/commentaries with valid text_id""" - mock_commentary_1 = TextDTO( - id="commentary-1-uuid", - pecha_text_id="commentary_pecha_1", - title="Commentary on Heart Sutra", - language="bo", - group_id="123e4567-e89b-12d3-a456-426614174000", - type="commentary", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="commentator_1", - categories=["123e4567-e89b-12d3-a456-426614174000"], - views=100, - source_link="https://commentary-source-1.com", - ranking=1, - license="CC0" - ) - - mock_commentary_2 = TextDTO( - id="commentary-2-uuid", - pecha_text_id="commentary_pecha_2", - title="Another Commentary on Heart Sutra", - language="bo", - group_id="123e4567-e89b-12d3-a456-426614174000", - type="commentary", - is_published=True, - created_date="2025-01-02T00:00:00", - updated_date="2025-01-02T00:00:00", - published_date="2025-01-02T00:00:00", - published_by="commentator_2", - categories=["123e4567-e89b-12d3-a456-426614174000"], - views=50, - source_link="https://commentary-source-2.com", - ranking=2, - license="CC0" - ) - - mock_commentaries = [mock_commentary_1, mock_commentary_2] - - mock_get_commentaries = mocker.patch( - 'pecha_api.texts.texts_views.get_commentaries_by_text_id', - return_value=mock_commentaries - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get( - "/texts/123e4567-e89b-12d3-a456-426614174000/commentaries", - params={"skip": 0, "limit": 10} - ) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert len(data) == 2 - assert data[0]["title"] == "Commentary on Heart Sutra" - assert data[0]["type"] == "commentary" - assert data[1]["title"] == "Another Commentary on Heart Sutra" - - mock_get_commentaries.assert_called_once_with( - text_id="123e4567-e89b-12d3-a456-426614174000", - skip=0, - limit=10 - ) - - -@pytest.mark.asyncio -async def test_get_commentaries_empty_list(mocker): - """Test GET /texts/{text_id}/commentaries when no commentaries exist""" - mock_get_commentaries = mocker.patch( - 'pecha_api.texts.texts_views.get_commentaries_by_text_id', - return_value=[] - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get( - "/texts/123e4567-e89b-12d3-a456-426614174000/commentaries", - params={"skip": 0, "limit": 10} - ) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert len(data) == 0 - assert data == [] - - mock_get_commentaries.assert_called_once() - - -@pytest.mark.asyncio -async def test_get_commentaries_with_pagination(mocker): - """Test GET /texts/{text_id}/commentaries with pagination parameters""" - mock_commentary = TextDTO( - id="commentary-uuid", - pecha_text_id="commentary_pecha", - title="Paginated Commentary", - language="bo", - group_id="123e4567-e89b-12d3-a456-426614174000", - type="commentary", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="commentator", - categories=["123e4567-e89b-12d3-a456-426614174000"], - views=10, - source_link="https://commentary-source.com", - ranking=1, - license="CC0" - ) - - mock_get_commentaries = mocker.patch( - 'pecha_api.texts.texts_views.get_commentaries_by_text_id', - return_value=[mock_commentary] - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get( - "/texts/123e4567-e89b-12d3-a456-426614174000/commentaries", - params={"skip": 5, "limit": 20} - ) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert len(data) == 1 - - mock_get_commentaries.assert_called_once_with( - text_id="123e4567-e89b-12d3-a456-426614174000", - skip=5, - limit=20 - ) - - -@pytest.mark.asyncio -async def test_get_commentaries_text_not_found(mocker): - """Test GET /texts/{text_id}/commentaries with non-existent text""" - mock_get_commentaries = mocker.patch( - 'pecha_api.texts.texts_views.get_commentaries_by_text_id', - side_effect=HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Text not found" - ) - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get( - "/texts/non-existent-text-id/commentaries", - params={"skip": 0, "limit": 10} - ) - - assert response.status_code == status.HTTP_404_NOT_FOUND - assert response.json()["detail"] == "Text not found" - - mock_get_commentaries.assert_called_once() - - -@pytest.mark.asyncio -async def test_get_commentaries_default_pagination(mocker): - """Test GET /texts/{text_id}/commentaries with default pagination values""" - mock_get_commentaries = mocker.patch( - 'pecha_api.texts.texts_views.get_commentaries_by_text_id', - return_value=[] - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get( - "/texts/123e4567-e89b-12d3-a456-426614174000/commentaries" - ) - - assert response.status_code == status.HTTP_200_OK - - mock_get_commentaries.assert_called_once_with( - text_id="123e4567-e89b-12d3-a456-426614174000", - skip=0, - limit=10 - ) - - -@pytest.mark.asyncio -async def test_get_commentaries_negative_skip(mocker): - """Test GET /texts/{text_id}/commentaries with negative skip parameter""" - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get( - "/texts/123e4567-e89b-12d3-a456-426614174000/commentaries", - params={"skip": -1, "limit": 10} - ) - - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - - -@pytest.mark.asyncio -async def test_get_commentaries_limit_exceeds_max(mocker): - """Test GET /texts/{text_id}/commentaries with limit exceeding maximum""" - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get( - "/texts/123e4567-e89b-12d3-a456-426614174000/commentaries", - params={"skip": 0, "limit": 150} - ) - - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - - -@pytest.mark.asyncio -async def test_get_commentaries_single_commentary(mocker): - """Test GET /texts/{text_id}/commentaries with single commentary""" - mock_commentary = TextDTO( - id="single-commentary-uuid", - pecha_text_id="single_commentary_pecha", - title="Single Commentary", - language="en", - group_id="123e4567-e89b-12d3-a456-426614174000", - type="commentary", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="single_commentator", - categories=["123e4567-e89b-12d3-a456-426614174000"], - views=5, - source_link="https://single-commentary.com", - ranking=1, - license="CC BY" - ) - - mock_get_commentaries = mocker.patch( - 'pecha_api.texts.texts_views.get_commentaries_by_text_id', - return_value=[mock_commentary] - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get( - "/texts/123e4567-e89b-12d3-a456-426614174000/commentaries", - params={"skip": 0, "limit": 10} - ) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert len(data) == 1 - assert data[0]["id"] == "single-commentary-uuid" - assert data[0]["title"] == "Single Commentary" - assert data[0]["type"] == "commentary" - assert data[0]["language"] == "en" - assert data[0]["categories"] == ["123e4567-e89b-12d3-a456-426614174000"] - - -@pytest.mark.asyncio -async def test_get_commentaries_with_optional_fields_none(mocker): - """Test GET /texts/{text_id}/commentaries with optional fields as None""" - mock_commentary = TextDTO( - id="commentary-optional-none-uuid", - pecha_text_id=None, - title="Commentary with None fields", - language="bo", - group_id="123e4567-e89b-12d3-a456-426614174000", - type="commentary", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="commentator", - categories=["123e4567-e89b-12d3-a456-426614174000"], - views=0, - source_link=None, - ranking=None, - license=None - ) - - mock_get_commentaries = mocker.patch( - 'pecha_api.texts.texts_views.get_commentaries_by_text_id', - return_value=[mock_commentary] - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get( - "/texts/123e4567-e89b-12d3-a456-426614174000/commentaries", - params={"skip": 0, "limit": 10} - ) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert len(data) == 1 - assert data[0]["pecha_text_id"] is None - assert data[0]["source_link"] is None - assert data[0]["ranking"] is None - assert data[0]["license"] is None - assert data[0]["views"] == 0 - - -@pytest.mark.asyncio -async def test_get_commentaries_service_error(mocker): - """Test GET /texts/{text_id}/commentaries when service raises unexpected error""" - mock_get_commentaries = mocker.patch( - 'pecha_api.texts.texts_views.get_commentaries_by_text_id', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Internal server error" - ) - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get( - "/texts/123e4567-e89b-12d3-a456-426614174000/commentaries", - params={"skip": 0, "limit": 10} - ) - - assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR - assert response.json()["detail"] == "Internal server error" - - -@pytest.mark.asyncio -async def test_get_contents_with_details_success(mocker): - """Test POST /texts/{text_id}/details with valid request""" - # Create mock response with all required fields - mock_detail_response = { - "text_detail": MOCK_TEXT_DTO.model_dump(), - "content": { - "id": "123e4567-e89b-12d3-a456-426614174001", - "text_id": "123e4567-e89b-12d3-a456-426614174000", - "sections": [] - }, - "size": 10, - "pagination_direction": "next", - "current_segment_position": 1, - "total_segments": 100 - } - - # Mock the service function - mock_get_details = mocker.patch( - 'pecha_api.texts.texts_views.get_text_details_by_text_id', - new_callable=AsyncMock, - return_value=mock_detail_response - ) - - # Test data - test_text_id = "123e4567-e89b-12d3-a456-426614174000" - details_request = { - "version_id": "123e4567-e89b-12d3-a456-426614174003", - "content_id": "123e4567-e89b-12d3-a456-426614174001", - "segment_id": "123e4567-e89b-12d3-a456-426614174005", - "size": 10, - "direction": "next" - } - - # Make the request - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post( - f"/texts/{test_text_id}/details", - json=details_request - ) - - # Assertions - assert response.status_code == 200 - response_data = response.json() - assert "text_detail" in response_data - assert "content" in response_data - assert "size" in response_data - assert "pagination_direction" in response_data - assert "current_segment_position" in response_data - assert "total_segments" in response_data - assert response_data["text_detail"]["id"] == MOCK_TEXT_DTO.id - assert response_data["size"] == 10 - assert response_data["total_segments"] == 100 - mock_get_details.assert_called_once() - call_args = mock_get_details.call_args[1] - assert call_args["text_id"] == test_text_id - assert isinstance(call_args["text_details_request"], TextDetailsRequest) - - -@pytest.mark.asyncio -async def test_get_contents_with_details_invalid_text(mocker): - """Test POST /texts/{text_id}/details with non-existent text""" - mocker.patch( - 'pecha_api.texts.texts_views.get_text_details_by_text_id', - side_effect=HTTPException(status_code=404, detail="Text not found") - ) - - details_request = { - "version_id": "123e4567-e89b-12d3-a456-426614174003", - "content_id": "123e4567-e89b-12d3-a456-426614174001", - "segment_id": "123e4567-e89b-12d3-a456-426614174005", - "size": 10, - "direction": "next" - } - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post( - "/texts/non_existent_id/details", - json=details_request - ) - - assert response.status_code == 404 - - -@pytest.mark.asyncio -async def test_get_versions_not_found(mocker): - """Test GET /texts/{text_id}/versions with non-existent text""" - mocker.patch( - 'pecha_api.texts.texts_views.get_text_versions_by_group_id', - side_effect=HTTPException(status_code=404, detail="Text not found") - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/non_existent_id/versions") - - assert response.status_code == 404 - - -@pytest.mark.asyncio -async def test_get_text_invalid_parameters(mocker): - """Test GET /texts with invalid pagination parameters""" - mock_get_text = mocker.patch( - 'pecha_api.texts.texts_views.get_text_by_text_id_or_collection', - new_callable=AsyncMock, - return_value=MOCK_TEXT_DTO - ) - - # Test with negative skip - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts?text_id=123&skip=-1&limit=10") - - # FastAPI will validate and return 422 for invalid parameters - assert response.status_code in [200, 422] # Depends on FastAPI validation - - -@pytest.mark.asyncio -async def test_create_text_invalid_data(): - """Test POST /texts with invalid data""" - invalid_data = { - "title": "Test", - # Missing required fields - } - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post( - "/texts", - json=invalid_data, - headers={"Authorization": f"Bearer {VALID_TOKEN}"} - ) - - # Should return 422 for validation error - assert response.status_code == 422 - - -@pytest.mark.asyncio -async def test_create_text_service_error(mocker): - """Test POST /texts when service layer raises an error""" - mocker.patch( - 'pecha_api.texts.texts_views.create_new_text', - side_effect=HTTPException(status_code=400, detail="Invalid group_id") - ) - - create_data = { - "pecha_text_id": "test_pecha_id", - "title": "Test Text", - "language": "bo", - "isPublished": True, - "group_id": "invalid_group_id", - "type": "version", - "published_by": "test_user", - "categories": [], - "source_link": "https://test-source.com", - "ranking": 1, - "license": "CC0" - } - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post( - "/texts", - json=create_data, - headers={"Authorization": f"Bearer {VALID_TOKEN}"} - ) - - assert response.status_code == 400 - assert response.json()["detail"] == "Invalid group_id" - - -@pytest.mark.asyncio -async def test_get_text_by_pecha_text_ids_success(mocker): - """Test POST /text-uploader/list with valid pecha_text_ids""" - mock_text_1 = TextDTO( - id="text-1-uuid", - pecha_text_id="pecha_text_id_1", - title="Test Text 1", - language="bo", - group_id="group-1-uuid", - type="root_text", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="test_user", - categories=[], - views=10, - source_link="https://test-source-1.com", - ranking=1, - license="CC0" - ) - - mock_text_2 = TextDTO( - id="text-2-uuid", - pecha_text_id="pecha_text_id_2", - title="Test Text 2", - language="en", - group_id="group-2-uuid", - type="version", - is_published=True, - created_date="2025-01-02T00:00:00", - updated_date="2025-01-02T00:00:00", - published_date="2025-01-02T00:00:00", - published_by="test_user_2", - categories=["category-1"], - views=20, - source_link="https://test-source-2.com", - ranking=2, - license="CC BY" - ) - - mock_texts = [mock_text_1, mock_text_2] - - mock_get_texts = mocker.patch( - 'pecha_api.text_uploader.text_metadata.text_metadata_views.get_text_by_pecha_text_ids_service', - new_callable=AsyncMock, - return_value=mock_texts - ) - - request_data = { - "pecha_text_ids": ["pecha_text_id_1", "pecha_text_id_2"] - } - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post( - "/text-uploader/list", - json=request_data - ) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert len(data) == 2 - assert data[0]["pecha_text_id"] == "pecha_text_id_1" - assert data[0]["title"] == "Test Text 1" - assert data[1]["pecha_text_id"] == "pecha_text_id_2" - assert data[1]["title"] == "Test Text 2" - - mock_get_texts.assert_called_once() - call_args = mock_get_texts.call_args[1] - assert isinstance(call_args["texts_by_pecha_text_ids_request"], TextsByPechaTextIdsRequest) - - -@pytest.mark.asyncio -async def test_get_text_by_pecha_text_ids_empty_list(mocker): - """Test POST /text-uploader/list with empty pecha_text_ids list""" - mock_get_texts = mocker.patch( - 'pecha_api.text_uploader.text_metadata.text_metadata_views.get_text_by_pecha_text_ids_service', - new_callable=AsyncMock, - return_value=[] - ) - - request_data = { - "pecha_text_ids": [] - } - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post( - "/text-uploader/list", - json=request_data - ) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert len(data) == 0 - assert data == [] - - mock_get_texts.assert_called_once() - - -@pytest.mark.asyncio -async def test_get_text_by_pecha_text_ids_single_text(mocker): - """Test POST /text-uploader/list with single pecha_text_id""" - mock_text = TextDTO( - id="single-text-uuid", - pecha_text_id="pecha_text_id_single", - title="Single Test Text", - language="bo", - group_id="group-uuid", - type="root_text", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="test_user", - categories=[], - views=5, - source_link="https://test-source.com", - ranking=1, - license="CC0" - ) - - mock_get_texts = mocker.patch( - 'pecha_api.text_uploader.text_metadata.text_metadata_views.get_text_by_pecha_text_ids_service', - new_callable=AsyncMock, - return_value=[mock_text] - ) - - request_data = { - "pecha_text_ids": ["pecha_text_id_single"] - } - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post( - "/text-uploader/list", - json=request_data - ) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert len(data) == 1 - assert data[0]["pecha_text_id"] == "pecha_text_id_single" - assert data[0]["title"] == "Single Test Text" - - mock_get_texts.assert_called_once() - - -@pytest.mark.asyncio -async def test_get_text_by_pecha_text_ids_none_result(mocker): - """Test POST /text-uploader/list when service returns None""" - mock_get_texts = mocker.patch( - 'pecha_api.text_uploader.text_metadata.text_metadata_views.get_text_by_pecha_text_ids_service', - new_callable=AsyncMock, - return_value=None - ) - - request_data = { - "pecha_text_ids": ["nonexistent_pecha_id"] - } - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post( - "/text-uploader/list", - json=request_data - ) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert data is None - - mock_get_texts.assert_called_once() - - -@pytest.mark.asyncio -async def test_get_text_by_pecha_text_ids_invalid_request(): - """Test POST /text-uploader/list with invalid request data""" - invalid_data = { - "invalid_field": ["pecha_text_id_1"] - } - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post( - "/text-uploader/list", - json=invalid_data - ) - - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - - -@pytest.mark.asyncio -async def test_get_text_by_pecha_text_ids_service_error(mocker): - """Test POST /text-uploader/list when service raises an error""" - mock_get_texts = mocker.patch( - 'pecha_api.text_uploader.text_metadata.text_metadata_views.get_text_by_pecha_text_ids_service', - new_callable=AsyncMock, - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Internal server error" - ) - ) - - request_data = { - "pecha_text_ids": ["pecha_text_id_1"] - } - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post( - "/text-uploader/list", - json=request_data - ) - - assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR - assert response.json()["detail"] == "Internal server error" - - -@pytest.mark.asyncio -async def test_get_text_by_pecha_text_ids_multiple_texts_with_optional_fields(mocker): - """Test POST /text-uploader/list with texts having various optional fields""" - mock_text_with_all_fields = TextDTO( - id="text-all-fields-uuid", - pecha_text_id="pecha_text_id_all", - title="Text with All Fields", - language="bo", - group_id="group-uuid", - type="root_text", - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="test_user", - categories=["cat1", "cat2"], - views=100, - source_link="https://source.com", - ranking=5, - license="CC BY-SA" - ) - - mock_text_with_minimal_fields = TextDTO( - id="text-minimal-uuid", - pecha_text_id=None, - title="Text with Minimal Fields", - language="en", - group_id="group-uuid-2", - type="version", - is_published=False, - created_date="2025-01-02T00:00:00", - updated_date="2025-01-02T00:00:00", - published_date="2025-01-02T00:00:00", - published_by="test_user_2", - categories=None, - views=0, - source_link=None, - ranking=None, - license=None - ) - - mock_texts = [mock_text_with_all_fields, mock_text_with_minimal_fields] - - mock_get_texts = mocker.patch( - 'pecha_api.text_uploader.text_metadata.text_metadata_views.get_text_by_pecha_text_ids_service', - new_callable=AsyncMock, - return_value=mock_texts - ) - - request_data = { - "pecha_text_ids": ["pecha_text_id_all", "pecha_text_id_minimal"] - } - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.post( - "/text-uploader/list", - json=request_data - ) - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert len(data) == 2 - - # Verify first text with all fields - assert data[0]["pecha_text_id"] == "pecha_text_id_all" - assert data[0]["categories"] == ["cat1", "cat2"] - assert data[0]["views"] == 100 - assert data[0]["ranking"] == 5 - - # Verify second text with minimal fields - assert data[1]["pecha_text_id"] is None - assert data[1]["categories"] is None - assert data[1]["views"] == 0 - assert data[1]["source_link"] is None - assert data[1]["ranking"] is None - assert data[1]["license"] is None - - -@pytest.mark.asyncio -async def test_search_titles_success(mocker): - """Test GET /texts/title-search returns title search results""" - mock_results = [ - TitleSearchResult(id="text_1", title="A Title"), - TitleSearchResult(id="text_2", title="Another Title") - ] - - mock_search_titles = mocker.patch( - "pecha_api.texts.texts_views.get_titles_and_ids_by_query", - new_callable=AsyncMock, - return_value=mock_results - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/title-search?title=Test&limit=20&offset=0") - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert data == [ - {"id": "text_1", "title": "A Title"}, - {"id": "text_2", "title": "Another Title"} - ] - - mock_search_titles.assert_called_once_with( - title="Test", - author=None, - limit=20, - offset=0 - ) - - -# ============================================================================ -# GET /texts/{text_id}/languages Tests -# ============================================================================ - -@pytest.mark.asyncio -async def test_get_languages_success(mocker): - """Test GET /texts/{text_id}/languages returns available languages""" - mock_response = LanguageResponse( - text_id="123e4567-e89b-12d3-a456-426614174000", - title="Test Text", - available_languages=[ - AvailableLanguage(language="bo", language_code="bo", version_count=3), - AvailableLanguage(language="en", language_code="en", version_count=2), - AvailableLanguage(language="zh", language_code="zh", version_count=1) - ] - ) - - mock_get_languages = mocker.patch( - 'pecha_api.texts.texts_views.get_text_languages', - new_callable=AsyncMock, - return_value=mock_response - ) - - test_text_id = "123e4567-e89b-12d3-a456-426614174000" - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get(f"/texts/{test_text_id}/languages") - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert data["text_id"] == test_text_id - assert data["title"] == "Test Text" - assert len(data["available_languages"]) == 3 - assert data["available_languages"][0]["language"] == "bo" - assert data["available_languages"][0]["version_count"] == 3 - - mock_get_languages.assert_called_once_with(text_id=test_text_id) - - -@pytest.mark.asyncio -async def test_get_languages_empty_list(mocker): - """Test GET /texts/{text_id}/languages when no languages available""" - mock_response = LanguageResponse( - text_id="123e4567-e89b-12d3-a456-426614174000", - title="Test Text", - available_languages=[] - ) - - mocker.patch( - 'pecha_api.texts.texts_views.get_text_languages', - new_callable=AsyncMock, - return_value=mock_response - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/123e4567-e89b-12d3-a456-426614174000/languages") - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert data["available_languages"] == [] - - -@pytest.mark.asyncio -async def test_get_languages_text_not_found(mocker): - """Test GET /texts/{text_id}/languages with non-existent text""" - mocker.patch( - 'pecha_api.texts.texts_views.get_text_languages', - side_effect=HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Text not found" - ) - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/non-existent-id/languages") - - assert response.status_code == status.HTTP_404_NOT_FOUND - assert response.json()["detail"] == "Text not found" - - -@pytest.mark.asyncio -async def test_get_languages_single_language(mocker): - """Test GET /texts/{text_id}/languages with single language""" - mock_response = LanguageResponse( - text_id="123e4567-e89b-12d3-a456-426614174000", - title="Single Language Text", - available_languages=[ - AvailableLanguage(language="bo", language_code="bo", version_count=5) - ] - ) - - mocker.patch( - 'pecha_api.texts.texts_views.get_text_languages', - new_callable=AsyncMock, - return_value=mock_response - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/123e4567-e89b-12d3-a456-426614174000/languages") - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert len(data["available_languages"]) == 1 - assert data["available_languages"][0]["language"] == "bo" - assert data["available_languages"][0]["version_count"] == 5 - - -@pytest.mark.asyncio -async def test_get_languages_service_error(mocker): - """Test GET /texts/{text_id}/languages when service raises error""" - mocker.patch( - 'pecha_api.texts.texts_views.get_text_languages', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Internal server error" - ) - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/123e4567-e89b-12d3-a456-426614174000/languages") - - assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR - assert response.json()["detail"] == "Internal server error" - - -# ============================================================================ -# GET /texts/{text_id}/languages/{language}/versions Tests -# ============================================================================ - -@pytest.mark.asyncio -async def test_get_language_versions_success(mocker): - """Test GET /texts/{text_id}/languages/{language}/versions returns versions for a language""" - mock_version_1 = VersionDetail( - id="version-1-uuid", - title="Version 1", - parent_id=None, - priority=None, - language="bo", - type="version", - group_id="group-1-uuid", - table_of_contents=["toc-1"], - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="test_user", - source_link="https://source-1.com", - ranking=1, - license="CC0", - is_selected=True - ) - - mock_version_2 = VersionDetail( - id="version-2-uuid", - title="Version 2", - parent_id=None, - priority=None, - language="bo", - type="version", - group_id="group-1-uuid", - table_of_contents=["toc-2"], - is_published=True, - created_date="2025-01-02T00:00:00", - updated_date="2025-01-02T00:00:00", - published_date="2025-01-02T00:00:00", - published_by="test_user_2", - source_link="https://source-2.com", - ranking=2, - license="CC BY", - is_selected=False - ) - - mock_response = VersionsResponse( - text_id="123e4567-e89b-12d3-a456-426614174000", - language="bo", - available_versions=[mock_version_1, mock_version_2] - ) - - mock_get_versions = mocker.patch( - 'pecha_api.texts.texts_views.get_language_versions', - new_callable=AsyncMock, - return_value=mock_response - ) - - test_text_id = "123e4567-e89b-12d3-a456-426614174000" - test_language = "bo" - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get(f"/texts/{test_text_id}/languages/{test_language}/versions") - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert data["text_id"] == test_text_id - assert data["language"] == test_language - assert len(data["available_versions"]) == 2 - assert data["available_versions"][0]["id"] == "version-1-uuid" - assert data["available_versions"][0]["is_selected"] == True - assert data["available_versions"][1]["id"] == "version-2-uuid" - assert data["available_versions"][1]["is_selected"] == False - - mock_get_versions.assert_called_once_with(text_id=test_text_id, language=test_language) - - -@pytest.mark.asyncio -async def test_get_language_versions_empty_list(mocker): - """Test GET /texts/{text_id}/languages/{language}/versions when no versions for language""" - mock_response = VersionsResponse( - text_id="123e4567-e89b-12d3-a456-426614174000", - language="zh", - available_versions=[] - ) - - mocker.patch( - 'pecha_api.texts.texts_views.get_language_versions', - new_callable=AsyncMock, - return_value=mock_response - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/123e4567-e89b-12d3-a456-426614174000/languages/zh/versions") - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert data["language"] == "zh" - assert data["available_versions"] == [] - - -@pytest.mark.asyncio -async def test_get_language_versions_text_not_found(mocker): - """Test GET /texts/{text_id}/languages/{language}/versions with non-existent text""" - mocker.patch( - 'pecha_api.texts.texts_views.get_language_versions', - side_effect=HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Text not found" - ) - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/non-existent-id/languages/bo/versions") - - assert response.status_code == status.HTTP_404_NOT_FOUND - assert response.json()["detail"] == "Text not found" - - -@pytest.mark.asyncio -async def test_get_language_versions_single_version(mocker): - """Test GET /texts/{text_id}/languages/{language}/versions with single version""" - mock_version = VersionDetail( - id="single-version-uuid", - title="Single Version", - parent_id=None, - priority=1, - language="en", - type="translation", - group_id="group-1-uuid", - table_of_contents=["toc-1", "toc-2"], - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="translator", - source_link="https://translation-source.com", - ranking=1, - license="CC BY-SA", - is_selected=True - ) - - mock_response = VersionsResponse( - text_id="123e4567-e89b-12d3-a456-426614174000", - language="en", - available_versions=[mock_version] - ) - - mocker.patch( - 'pecha_api.texts.texts_views.get_language_versions', - new_callable=AsyncMock, - return_value=mock_response - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/123e4567-e89b-12d3-a456-426614174000/languages/en/versions") - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert len(data["available_versions"]) == 1 - assert data["available_versions"][0]["id"] == "single-version-uuid" - assert data["available_versions"][0]["language"] == "en" - assert data["available_versions"][0]["type"] == "translation" - assert data["available_versions"][0]["is_selected"] == True - - -@pytest.mark.asyncio -async def test_get_language_versions_service_error(mocker): - """Test GET /texts/{text_id}/languages/{language}/versions when service raises error""" - mocker.patch( - 'pecha_api.texts.texts_views.get_language_versions', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Internal server error" - ) - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/123e4567-e89b-12d3-a456-426614174000/languages/bo/versions") - - assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR - assert response.json()["detail"] == "Internal server error" - - -# ============================================================================ -# GET /texts/versions/{version_id}/info Tests -# ============================================================================ - -@pytest.mark.asyncio -async def test_get_version_info_success(mocker): - """Test GET /texts/versions/{version_id}/info returns version details""" - mock_version = VersionDetail( - id="123e4567-e89b-12d3-a456-426614174000", - title="Test Version", - parent_id=None, - priority=None, - language="bo", - type="version", - group_id="group-1-uuid", - table_of_contents=["toc-1", "toc-2"], - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="test_user", - source_link="https://source.com", - ranking=1, - license="CC0", - is_selected=True - ) - - mock_get_version_info = mocker.patch( - 'pecha_api.texts.texts_views.get_version_info', - new_callable=AsyncMock, - return_value=mock_version - ) - - test_version_id = "123e4567-e89b-12d3-a456-426614174000" - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get(f"/texts/versions/{test_version_id}/info") - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert data["id"] == test_version_id - assert data["title"] == "Test Version" - assert data["language"] == "bo" - assert data["type"] == "version" - assert data["group_id"] == "group-1-uuid" - assert data["table_of_contents"] == ["toc-1", "toc-2"] - assert data["is_published"] == True - assert data["is_selected"] == True - assert data["source_link"] == "https://source.com" - assert data["ranking"] == 1 - assert data["license"] == "CC0" - - mock_get_version_info.assert_called_once_with(version_id=test_version_id) - - -@pytest.mark.asyncio -async def test_get_version_info_not_found(mocker): - """Test GET /texts/versions/{version_id}/info with non-existent version""" - mocker.patch( - 'pecha_api.texts.texts_views.get_version_info', - side_effect=HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Text not found" - ) - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/versions/non-existent-id/info") - - assert response.status_code == status.HTTP_404_NOT_FOUND - assert response.json()["detail"] == "Text not found" - - -@pytest.mark.asyncio -async def test_get_version_info_with_optional_fields_none(mocker): - """Test GET /texts/versions/{version_id}/info with optional fields as None""" - mock_version = VersionDetail( - id="123e4567-e89b-12d3-a456-426614174000", - title="Version with None fields", - parent_id=None, - priority=None, - language="bo", - type="version", - group_id="group-1-uuid", - table_of_contents=[], - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="test_user", - source_link=None, - ranking=None, - license=None, - is_selected=True - ) - - mocker.patch( - 'pecha_api.texts.texts_views.get_version_info', - new_callable=AsyncMock, - return_value=mock_version - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/versions/123e4567-e89b-12d3-a456-426614174000/info") - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert data["parent_id"] is None - assert data["priority"] is None - assert data["source_link"] is None - assert data["ranking"] is None - assert data["license"] is None - assert data["table_of_contents"] == [] - - -@pytest.mark.asyncio -async def test_get_version_info_with_multiple_toc(mocker): - """Test GET /texts/versions/{version_id}/info with multiple table of contents""" - mock_version = VersionDetail( - id="123e4567-e89b-12d3-a456-426614174000", - title="Version with multiple TOC", - parent_id=None, - priority=1, - language="en", - type="translation", - group_id="group-1-uuid", - table_of_contents=["toc-1", "toc-2", "toc-3"], - is_published=True, - created_date="2025-01-01T00:00:00", - updated_date="2025-01-01T00:00:00", - published_date="2025-01-01T00:00:00", - published_by="translator", - source_link="https://translation.com", - ranking=2, - license="CC BY", - is_selected=True - ) - - mocker.patch( - 'pecha_api.texts.texts_views.get_version_info', - new_callable=AsyncMock, - return_value=mock_version - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/versions/123e4567-e89b-12d3-a456-426614174000/info") - - assert response.status_code == status.HTTP_200_OK - data = response.json() - assert len(data["table_of_contents"]) == 3 - assert data["table_of_contents"] == ["toc-1", "toc-2", "toc-3"] - assert data["type"] == "translation" - assert data["priority"] == 1 - - -@pytest.mark.asyncio -async def test_get_version_info_service_error(mocker): - """Test GET /texts/versions/{version_id}/info when service raises error""" - mocker.patch( - 'pecha_api.texts.texts_views.get_version_info', - side_effect=HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Internal server error" - ) - ) - - async with AsyncClient(transport=ASGITransport(app=api), base_url="http://test") as ac: - response = await ac.get("/texts/versions/123e4567-e89b-12d3-a456-426614174000/info") - - assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR - assert response.json()["detail"] == "Internal server error"