import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) import pytest import hmac from unittest.mock import Mock, MagicMock, patch class TestCheckPassword: """Tests for check_password function.""" @patch("app.st") def test_returns_true_when_already_authenticated(self, mock_st): """Should return True if password_correct is True.""" from app import check_password mock_st.session_state = MagicMock() mock_st.session_state.__contains__ = Mock(return_value=True) mock_st.session_state.password_correct = True result = check_password() assert result is True @patch("app.APP_PASSWORD", "") @patch("app.st") def test_returns_true_when_no_password_configured(self, mock_st): """Should return True when APP_PASSWORD is empty.""" from app import check_password mock_st.session_state = MagicMock() mock_st.session_state.__contains__ = Mock(return_value=False) result = check_password() assert result is True @patch("app.APP_PASSWORD", "correct") @patch("app.st") def test_returns_false_when_wrong_password(self, mock_st): """Should return False for wrong password.""" from app import check_password mock_st.session_state = MagicMock() mock_st.session_state.__contains__ = Mock(return_value=False) mock_st.text_input.return_value = "wrong" result = check_password() assert result is False mock_st.error.assert_called_once() @patch("app.APP_PASSWORD", "correct") @patch("app.st") def test_returns_false_when_no_password_entered(self, mock_st): """Should return False when no password entered.""" from app import check_password mock_st.session_state = MagicMock() mock_st.session_state.__contains__ = Mock(return_value=False) mock_st.text_input.return_value = "" result = check_password() assert result is False class TestProcessUploadedFiles: """Tests for process_uploaded_files function.""" @patch("app.get_vector_store") @patch("app.extract_pdf_pages") @patch("app.process_documents") @patch("app.chunks_to_store_format") def test_processes_single_file(self, mock_format, mock_process, mock_extract, mock_get_store): """Should process a single PDF file.""" from app import process_uploaded_files mock_file = Mock() mock_file.name = "test.pdf" mock_extract.return_value = [{"text": "content", "page_number": 1, "source": "test.pdf"}] mock_process.return_value = [{"text": "chunk", "metadata": {}}] mock_format.return_value = (["chunk"], [{}]) mock_store = Mock() mock_store.get_loaded_sources.return_value = set() mock_get_store.return_value = mock_store result = process_uploaded_files([mock_file]) assert result == 1 mock_store.add_documents.assert_called_once() @patch("app.get_vector_store") @patch("app.extract_pdf_pages") @patch("app.process_documents") @patch("app.chunks_to_store_format") def test_processes_multiple_files(self, mock_format, mock_process, mock_extract, mock_get_store): """Should process multiple PDF files.""" from app import process_uploaded_files mock_file1 = Mock() mock_file1.name = "test1.pdf" mock_file2 = Mock() mock_file2.name = "test2.pdf" mock_extract.return_value = [{"text": "content", "page_number": 1, "source": "test.pdf"}] mock_process.return_value = [{"text": "chunk", "metadata": {}}] mock_format.return_value = (["chunk"], [{}]) mock_store = Mock() mock_store.get_loaded_sources.return_value = set() mock_get_store.return_value = mock_store result = process_uploaded_files([mock_file1, mock_file2]) assert result == 2 assert mock_store.add_documents.call_count == 2 def test_empty_files_returns_zero(self): """Should return 0 for empty file list.""" from app import process_uploaded_files result = process_uploaded_files([]) assert result == 0 def test_none_files_returns_zero(self): """Should return 0 for None.""" from app import process_uploaded_files result = process_uploaded_files(None) assert result == 0 @patch("app.get_vector_store") @patch("app.extract_pdf_pages") @patch("app.process_documents") def test_skips_empty_chunks(self, mock_process, mock_extract, mock_get_store): """Should skip files with no chunks.""" from app import process_uploaded_files mock_file = Mock() mock_file.name = "empty.pdf" mock_extract.return_value = [] mock_process.return_value = [] mock_store = Mock() mock_store.get_loaded_sources.return_value = set() mock_get_store.return_value = mock_store result = process_uploaded_files([mock_file]) assert result == 0 mock_store.add_documents.assert_not_called() class TestResetApplication: """Tests for reset_application function.""" @patch("app.reset_llm_generator") @patch("app.reset_retrieval_engine") @patch("app.reset_vector_store") @patch("app.reset_embedder") @patch("app.st") def test_resets_all_components(self, mock_st, mock_reset_embedder, mock_reset_store, mock_reset_retrieval, mock_reset_llm): """Should reset vector store and embedder.""" from app import reset_application mock_st.session_state = MagicMock() reset_application() mock_reset_store.assert_called_once() mock_reset_embedder.assert_called_once() mock_reset_retrieval.assert_called_once() mock_reset_llm.assert_called_once() @patch("app.reset_llm_generator") @patch("app.reset_retrieval_engine") @patch("app.reset_vector_store") @patch("app.reset_embedder") @patch("app.st") def test_clears_session_state(self, mock_st, mock_reset_embedder, mock_reset_store, mock_reset_retrieval, mock_reset_llm): """Should clear session state values.""" from app import reset_application mock_st.session_state = MagicMock() mock_st.session_state.documents_processed = True mock_st.session_state.chunk_count = 100 mock_st.session_state.chat_history = [{"role": "user", "content": "hi"}] mock_st.session_state.sources = [{}] reset_application() assert mock_st.session_state.documents_processed is False assert mock_st.session_state.chunk_count == 0 assert mock_st.session_state.chat_history == [] assert mock_st.session_state.sources == [] class TestInitSessionState: """Tests for init_session_state function.""" @patch("app.st") def test_initializes_missing_keys(self, mock_st): """Should initialize missing session state keys.""" from app import init_session_state mock_st.session_state = {} init_session_state() assert "documents_processed" in mock_st.session_state assert "chunk_count" in mock_st.session_state assert "chat_history" in mock_st.session_state assert "selected_model" in mock_st.session_state assert "use_reranking" in mock_st.session_state @patch("app.st") def test_preserves_existing_values(self, mock_st): """Should not overwrite existing session state values.""" from app import init_session_state mock_st.session_state = {"chunk_count": 50} init_session_state() assert mock_st.session_state["chunk_count"] == 50 @patch("app.st") def test_default_values(self, mock_st): """Should set correct default values.""" from app import init_session_state from config import DEFAULT_LLM_MODEL mock_st.session_state = {} init_session_state() assert mock_st.session_state["documents_processed"] is False assert mock_st.session_state["chunk_count"] == 0 assert mock_st.session_state["chat_history"] == [] assert mock_st.session_state["use_reranking"] is True assert mock_st.session_state["selected_model"] == DEFAULT_LLM_MODEL class TestGenerateResponse: """Tests for generate_response function.""" @pytest.fixture(autouse=True) def reset_handler(self): from src.integration.query_handler import reset_query_handler reset_query_handler() yield reset_query_handler() @patch("app.get_query_handler") def test_returns_response_sources_debug(self, mock_get_handler): from app import generate_response mock_handler = MagicMock() mock_result = MagicMock() mock_result.response = "response text" mock_result.sources = [{"source": "test.pdf"}] mock_result.mode.value = "documents" mock_result.execution_time = 0.5 mock_result.intent = None mock_result.sql_query = None mock_result.db_results = None mock_result.success = True mock_result.error = None mock_result.routing_metadata = None mock_handler.execute.return_value = mock_result mock_get_handler.return_value = mock_handler response, sources, debug = generate_response("question", True, [], "model-id") assert response == "response text" assert sources == [{"source": "test.pdf"}] assert debug["success"] is True @patch("app.get_query_handler") def test_passes_reranking_flag(self, mock_get_handler): from app import generate_response mock_handler = MagicMock() mock_result = MagicMock() mock_result.response = "" mock_result.sources = [] mock_result.mode.value = "documents" mock_result.execution_time = 0.1 mock_result.intent = None mock_result.sql_query = None mock_result.db_results = None mock_result.success = True mock_result.error = None mock_result.routing_metadata = None mock_handler.execute.return_value = mock_result mock_get_handler.return_value = mock_handler generate_response("q", False, [], "model") mock_handler.execute.assert_called_once() @patch("app.get_query_handler") def test_passes_chat_history_to_retrieve(self, mock_get_handler): from app import generate_response mock_handler = MagicMock() mock_result = MagicMock() mock_result.response = "" mock_result.sources = [] mock_result.mode.value = "documents" mock_result.execution_time = 0.1 mock_result.intent = None mock_result.sql_query = None mock_result.db_results = None mock_result.success = True mock_result.error = None mock_result.routing_metadata = None mock_handler.execute.return_value = mock_result mock_get_handler.return_value = mock_handler history = [{"role": "user", "content": "Atlas nedir?"}] generate_response("Bunun ozellikleri?", True, history, "model") call_kwargs = mock_handler.execute.call_args assert call_kwargs[1]["chat_history"] == history @patch("app.get_query_handler") def test_limits_chat_history(self, mock_get_handler): from app import generate_response mock_handler = MagicMock() mock_result = MagicMock() mock_result.response = "" mock_result.sources = [] mock_result.mode.value = "documents" mock_result.execution_time = 0.1 mock_result.intent = None mock_result.sql_query = None mock_result.db_results = None mock_result.success = True mock_result.error = None mock_result.routing_metadata = None mock_handler.execute.return_value = mock_result mock_get_handler.return_value = mock_handler long_history = [{"role": "user", "content": f"msg{i}"} for i in range(10)] generate_response("q", True, long_history, "model") call_kwargs = mock_handler.execute.call_args assert call_kwargs[1]["chat_history"] == long_history @patch("app.get_query_handler") def test_passes_model_id(self, mock_get_handler): from app import generate_response mock_handler = MagicMock() mock_result = MagicMock() mock_result.response = "" mock_result.sources = [] mock_result.mode.value = "documents" mock_result.execution_time = 0.1 mock_result.intent = None mock_result.sql_query = None mock_result.db_results = None mock_result.success = True mock_result.error = None mock_result.routing_metadata = None mock_handler.execute.return_value = mock_result mock_get_handler.return_value = mock_handler generate_response("q", True, [], "custom-model") call_kwargs = mock_handler.execute.call_args assert call_kwargs[1]["model_id"] == "custom-model" class TestLoadCss: """Tests for load_css function.""" @patch("app.STYLES_PATH") def test_loads_css_from_file(self, mock_path): """Should load CSS content from file.""" from app import load_css mock_path.exists.return_value = True mock_path.read_text.return_value = ".test { color: red; }" result = load_css() assert result == ".test { color: red; }" @patch("app.STYLES_PATH") def test_returns_empty_if_file_missing(self, mock_path): """Should return empty string if file doesn't exist.""" from app import load_css mock_path.exists.return_value = False result = load_css() assert result == "" class TestApplyCustomCss: """Tests for apply_custom_css function.""" @patch("app.load_css") @patch("app.st") def test_calls_markdown_with_css(self, mock_st, mock_load): """Should inject CSS via st.markdown.""" from app import apply_custom_css mock_load.return_value = ".test { color: red; }" apply_custom_css() mock_st.markdown.assert_called_once() call_args = mock_st.markdown.call_args assert "unsafe_allow_html" in call_args[1] assert call_args[1]["unsafe_allow_html"] is True @patch("app.load_css") @patch("app.st") def test_skips_if_no_css(self, mock_st, mock_load): """Should not call markdown if CSS is empty.""" from app import apply_custom_css mock_load.return_value = "" apply_custom_css() mock_st.markdown.assert_not_called() @patch("app.load_css") @patch("app.st") def test_wraps_css_in_style_tags(self, mock_st, mock_load): """Should wrap CSS content in style tags.""" from app import apply_custom_css mock_load.return_value = ".box { padding: 10px; }" apply_custom_css() call_args = mock_st.markdown.call_args[0][0] assert "" in call_args assert ".box { padding: 10px; }" in call_args class TestGetBaseDocuments: """Tests for get_base_documents function.""" @patch("app.BASE_DOCUMENTS_PATH") def test_returns_empty_if_path_not_exists(self, mock_path): """Should return empty list if documents folder doesn't exist.""" from app import get_base_documents mock_path.exists.return_value = False result = get_base_documents() assert result == [] @patch("app.BASE_DOCUMENTS_PATH") def test_returns_pdf_files(self, mock_path): """Should return list of PDF files.""" from app import get_base_documents mock_path.exists.return_value = True mock_path.glob.return_value = [Path("/docs/a.pdf"), Path("/docs/b.pdf")] result = get_base_documents() assert len(result) == 2 mock_path.glob.assert_called_with("*.pdf") class TestLoadDefaultDocuments: """Tests for load_default_documents function.""" @patch("app.get_base_documents") @patch("app.st") def test_skips_if_already_loaded(self, mock_st, mock_get_docs): """Should skip if base_docs_loaded is True.""" from app import load_default_documents mock_st.session_state.get.return_value = True load_default_documents() mock_get_docs.assert_not_called() @patch("app.get_vector_store") @patch("app.get_base_documents") @patch("app.st") def test_skips_if_no_documents(self, mock_st, mock_get_docs, mock_get_store): """Should set flag and return if no documents.""" from app import load_default_documents mock_st.session_state = MagicMock() mock_st.session_state.get.return_value = False mock_get_docs.return_value = [] load_default_documents() assert mock_st.session_state.base_docs_loaded is True @patch("app.chunks_to_store_format") @patch("app.process_documents") @patch("app.extract_pdf_from_path") @patch("app.get_vector_store") @patch("app.get_base_documents") @patch("app.st") def test_processes_documents( self, mock_st, mock_get_docs, mock_get_store, mock_extract, mock_process, mock_format ): """Should process and store base documents.""" from app import load_default_documents mock_st.session_state = MagicMock() mock_st.session_state.get.return_value = False mock_get_docs.return_value = [Path("/docs/test.pdf")] mock_store = MagicMock() mock_store.get_collection_stats.return_value = {"points_count": 0} mock_get_store.return_value = mock_store mock_extract.return_value = [{"text": "content", "page_number": 1, "source": "test.pdf"}] mock_process.return_value = [{"text": "chunk", "metadata": {}}] mock_format.return_value = (["chunk"], [{}]) load_default_documents() mock_store.add_documents.assert_called_once() assert mock_st.session_state.base_docs_loaded is True assert mock_st.session_state.documents_processed is True class TestRenderDebugInfo: """Tests for render_debug_info function.""" @patch("app.st") def test_shows_query_rewriting_when_rewritten(self, mock_st): """Should display query rewriting section when query was rewritten.""" from app import render_debug_info mock_col = MagicMock() mock_col.__enter__ = MagicMock(return_value=mock_col) mock_col.__exit__ = MagicMock(return_value=False) mock_st.columns.side_effect = [ [mock_col, mock_col], [mock_col, mock_col, mock_col] ] debug_info = { "query": "Bunun ozellikleri?", "search_query": "Atlas ERP ozellikleri nelerdir?", "query_rewritten": True, "rewrite_info": { "is_follow_up": True, "method": "llm", "rewrite_time_ms": 150 } } render_debug_info(debug_info) mock_st.subheader.assert_called() assert mock_st.columns.call_count == 2 @patch("app.st") def test_skips_rewriting_section_when_not_rewritten(self, mock_st): """Should skip query rewriting section when query was not rewritten.""" from app import render_debug_info debug_info = { "query": "Atlas nedir?", "search_query": "Atlas nedir?", "query_rewritten": False, "rewrite_info": None } render_debug_info(debug_info) mock_st.info.assert_not_called() mock_st.success.assert_not_called() @patch("app.st") def test_always_shows_retrieval_details(self, mock_st): """Should always show retrieval details JSON.""" from app import render_debug_info debug_info = { "query": "Test", "query_rewritten": False, "stage1_count": 10 } render_debug_info(debug_info) mock_st.json.assert_called_once()