iatoolkit 0.8.1__py3-none-any.whl → 0.63.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of iatoolkit might be problematic. Click here for more details.

Files changed (159) hide show
  1. iatoolkit/__init__.py +8 -34
  2. iatoolkit/base_company.py +14 -3
  3. iatoolkit/common/routes.py +83 -52
  4. iatoolkit/common/session_manager.py +0 -1
  5. iatoolkit/common/util.py +0 -27
  6. iatoolkit/iatoolkit.py +61 -46
  7. iatoolkit/infra/llm_client.py +7 -8
  8. iatoolkit/infra/openai_adapter.py +1 -1
  9. iatoolkit/infra/redis_session_manager.py +48 -2
  10. iatoolkit/repositories/database_manager.py +17 -2
  11. iatoolkit/repositories/models.py +31 -6
  12. iatoolkit/repositories/profile_repo.py +7 -2
  13. iatoolkit/services/auth_service.py +188 -0
  14. iatoolkit/services/branding_service.py +147 -0
  15. iatoolkit/services/dispatcher_service.py +10 -40
  16. iatoolkit/services/excel_service.py +15 -15
  17. iatoolkit/services/history_service.py +3 -12
  18. iatoolkit/services/jwt_service.py +15 -24
  19. iatoolkit/services/onboarding_service.py +43 -0
  20. iatoolkit/services/profile_service.py +97 -44
  21. iatoolkit/services/query_service.py +124 -81
  22. iatoolkit/services/tasks_service.py +1 -1
  23. iatoolkit/services/user_feedback_service.py +67 -31
  24. iatoolkit/services/user_session_context_service.py +112 -54
  25. iatoolkit/static/images/fernando.jpeg +0 -0
  26. iatoolkit/static/js/{chat_feedback.js → chat_feedback_button.js} +6 -11
  27. iatoolkit/static/js/chat_history_button.js +126 -0
  28. iatoolkit/static/js/chat_logout_button.js +36 -0
  29. iatoolkit/static/js/chat_main.js +130 -220
  30. iatoolkit/static/js/chat_onboarding_button.js +97 -0
  31. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  32. iatoolkit/static/js/chat_reload_button.js +52 -0
  33. iatoolkit/static/styles/chat_iatoolkit.css +329 -507
  34. iatoolkit/static/styles/chat_modal.css +95 -56
  35. iatoolkit/static/styles/landing_page.css +182 -0
  36. iatoolkit/static/styles/onboarding.css +169 -0
  37. iatoolkit/system_prompts/query_main.prompt +3 -12
  38. iatoolkit/templates/_company_header.html +20 -0
  39. iatoolkit/templates/_login_widget.html +40 -0
  40. iatoolkit/templates/base.html +8 -3
  41. iatoolkit/templates/change_password.html +54 -37
  42. iatoolkit/templates/chat.html +149 -66
  43. iatoolkit/templates/chat_modals.html +47 -18
  44. iatoolkit/templates/error.html +41 -8
  45. iatoolkit/templates/forgot_password.html +37 -24
  46. iatoolkit/templates/index.html +140 -0
  47. iatoolkit/templates/login_simulation.html +34 -0
  48. iatoolkit/templates/onboarding_shell.html +105 -0
  49. iatoolkit/templates/signup.html +64 -66
  50. iatoolkit/views/base_login_view.py +81 -0
  51. iatoolkit/views/change_password_view.py +23 -12
  52. iatoolkit/views/external_login_view.py +61 -28
  53. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +9 -2
  54. iatoolkit/views/forgot_password_view.py +23 -13
  55. iatoolkit/views/history_api_view.py +52 -0
  56. iatoolkit/views/home_view.py +58 -25
  57. iatoolkit/views/index_view.py +14 -0
  58. iatoolkit/views/init_context_api_view.py +68 -0
  59. iatoolkit/views/llmquery_api_view.py +45 -0
  60. iatoolkit/views/login_simulation_view.py +81 -0
  61. iatoolkit/views/login_view.py +118 -34
  62. iatoolkit/views/logout_api_view.py +45 -0
  63. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +7 -7
  64. iatoolkit/views/signup_view.py +38 -29
  65. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  66. iatoolkit/views/tasks_review_api_view.py +55 -0
  67. iatoolkit/views/{user_feedback_view.py → user_feedback_api_view.py} +16 -31
  68. iatoolkit/views/verify_user_view.py +13 -8
  69. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/METADATA +2 -2
  70. iatoolkit-0.63.4.dist-info/RECORD +113 -0
  71. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/top_level.txt +0 -1
  72. iatoolkit/common/auth.py +0 -200
  73. iatoolkit/static/images/arrow_up.png +0 -0
  74. iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
  75. iatoolkit/static/images/logo_clinica.png +0 -0
  76. iatoolkit/static/images/logo_iatoolkit.png +0 -0
  77. iatoolkit/static/images/logo_maxxa.png +0 -0
  78. iatoolkit/static/images/logo_notaria.png +0 -0
  79. iatoolkit/static/images/logo_tarjeta.png +0 -0
  80. iatoolkit/static/images/logo_umayor.png +0 -0
  81. iatoolkit/static/images/upload.png +0 -0
  82. iatoolkit/static/js/chat_history.js +0 -117
  83. iatoolkit/templates/home.html +0 -201
  84. iatoolkit/templates/login.html +0 -43
  85. iatoolkit/views/chat_token_request_view.py +0 -98
  86. iatoolkit/views/chat_view.py +0 -51
  87. iatoolkit/views/download_file_view.py +0 -58
  88. iatoolkit/views/external_chat_login_view.py +0 -88
  89. iatoolkit/views/history_view.py +0 -57
  90. iatoolkit/views/llmquery_view.py +0 -65
  91. iatoolkit/views/tasks_review_view.py +0 -83
  92. iatoolkit-0.8.1.dist-info/RECORD +0 -175
  93. tests/__init__.py +0 -5
  94. tests/common/__init__.py +0 -0
  95. tests/common/test_auth.py +0 -279
  96. tests/common/test_routes.py +0 -42
  97. tests/common/test_session_manager.py +0 -59
  98. tests/common/test_util.py +0 -444
  99. tests/companies/__init__.py +0 -5
  100. tests/conftest.py +0 -36
  101. tests/infra/__init__.py +0 -5
  102. tests/infra/connectors/__init__.py +0 -5
  103. tests/infra/connectors/test_google_drive_connector.py +0 -107
  104. tests/infra/connectors/test_local_file_connector.py +0 -85
  105. tests/infra/connectors/test_s3_connector.py +0 -95
  106. tests/infra/test_call_service.py +0 -92
  107. tests/infra/test_database_manager.py +0 -59
  108. tests/infra/test_gemini_adapter.py +0 -137
  109. tests/infra/test_google_chat_app.py +0 -68
  110. tests/infra/test_llm_client.py +0 -165
  111. tests/infra/test_llm_proxy.py +0 -122
  112. tests/infra/test_mail_app.py +0 -94
  113. tests/infra/test_openai_adapter.py +0 -105
  114. tests/infra/test_redis_session_manager_service.py +0 -117
  115. tests/repositories/__init__.py +0 -5
  116. tests/repositories/test_database_manager.py +0 -87
  117. tests/repositories/test_document_repo.py +0 -76
  118. tests/repositories/test_llm_query_repo.py +0 -340
  119. tests/repositories/test_models.py +0 -38
  120. tests/repositories/test_profile_repo.py +0 -142
  121. tests/repositories/test_tasks_repo.py +0 -76
  122. tests/repositories/test_vs_repo.py +0 -107
  123. tests/services/__init__.py +0 -5
  124. tests/services/test_dispatcher_service.py +0 -274
  125. tests/services/test_document_service.py +0 -181
  126. tests/services/test_excel_service.py +0 -208
  127. tests/services/test_file_processor_service.py +0 -121
  128. tests/services/test_history_service.py +0 -164
  129. tests/services/test_jwt_service.py +0 -255
  130. tests/services/test_load_documents_service.py +0 -112
  131. tests/services/test_mail_service.py +0 -70
  132. tests/services/test_profile_service.py +0 -379
  133. tests/services/test_prompt_manager_service.py +0 -190
  134. tests/services/test_query_service.py +0 -243
  135. tests/services/test_search_service.py +0 -39
  136. tests/services/test_sql_service.py +0 -160
  137. tests/services/test_tasks_service.py +0 -252
  138. tests/services/test_user_feedback_service.py +0 -389
  139. tests/services/test_user_session_context_service.py +0 -132
  140. tests/views/__init__.py +0 -5
  141. tests/views/test_change_password_view.py +0 -191
  142. tests/views/test_chat_token_request_view.py +0 -188
  143. tests/views/test_chat_view.py +0 -98
  144. tests/views/test_download_file_view.py +0 -149
  145. tests/views/test_external_chat_login_view.py +0 -120
  146. tests/views/test_external_login_view.py +0 -102
  147. tests/views/test_file_store_view.py +0 -128
  148. tests/views/test_forgot_password_view.py +0 -142
  149. tests/views/test_history_view.py +0 -336
  150. tests/views/test_home_view.py +0 -61
  151. tests/views/test_llm_query_view.py +0 -154
  152. tests/views/test_login_view.py +0 -114
  153. tests/views/test_prompt_view.py +0 -111
  154. tests/views/test_signup_view.py +0 -140
  155. tests/views/test_tasks_review_view.py +0 -104
  156. tests/views/test_tasks_view.py +0 -130
  157. tests/views/test_user_feedback_view.py +0 -214
  158. tests/views/test_verify_user_view.py +0 -110
  159. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/WHEEL +0 -0
@@ -1,243 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
-
4
- import pytest
5
- from unittest.mock import MagicMock, patch
6
- import os
7
- import base64
8
- import json
9
-
10
- from iatoolkit.services.query_service import QueryService
11
- from iatoolkit.services.prompt_manager_service import PromptService
12
- from iatoolkit.services.user_session_context_service import UserSessionContextService
13
- from iatoolkit.repositories.profile_repo import ProfileRepo
14
- from iatoolkit.repositories.models import User, Company
15
- from iatoolkit.common.util import Utility
16
- from iatoolkit.infra.llm_client import llmClient
17
- from iatoolkit.common.exceptions import IAToolkitException
18
-
19
-
20
- class TestQueryService:
21
- """
22
- Test suite for the QueryService.
23
- It uses a consistent setup to mock all dependencies and the global `current_iatoolkit` context.
24
- """
25
-
26
- @pytest.fixture(autouse=True)
27
- def setup(self):
28
- """
29
- Set up a consistent, mocked environment for each test.
30
- This includes mocking all dependencies and the global `current_iatoolkit` context.
31
- """
32
- # Mocks for all injected dependencies
33
- self.document_service = MagicMock()
34
- self.llmquery_repo = MagicMock()
35
- self.profile_repo = MagicMock(spec=ProfileRepo)
36
- self.prompt_service = MagicMock(spec=PromptService)
37
- self.utility = MagicMock(spec=Utility)
38
- self.llm_client_mock = MagicMock(spec=llmClient)
39
- self.dispatcher = MagicMock()
40
- self.session_context = MagicMock(spec=UserSessionContextService)
41
-
42
-
43
- # --- Common mock configurations ---
44
- self.utility.resolve_user_identifier.side_effect = lambda external_user_id=None,local_user_id=0: (external_user_id, False) or (f"User_{local_user_id}", True)
45
-
46
- self.user = User(id=1, email='test@user.com')
47
- self.company = Company(id=100, name='Test Company', short_name='test_company')
48
- self.profile_repo.get_company_by_short_name.return_value = self.company
49
-
50
- # Default mock session data
51
- self.session_context.get_last_response_id.return_value = 'prev_response_id'
52
- self.session_context.get_user_session_data.return_value = {'user_role': 'leader', 'user_name': 'session_user'}
53
-
54
- # Default mock LLM response
55
- self.mock_llm_response = {"valid_response": True, "answer": "LLM test response",
56
- "response_id": "new_llm_response_id"}
57
- self.llm_client_mock.invoke.return_value = self.mock_llm_response
58
- self.llm_client_mock.set_company_context.return_value = 'new_context_response_id'
59
-
60
- # Default mock Dispatcher responses
61
- self.dispatcher.get_user_info.return_value = {'user_role': 'leader', 'user_name': 'test_user'}
62
- self.dispatcher.get_company_services.return_value = [{'name': 'service1'}]
63
- self.dispatcher.get_company_context.return_value = "Specific company context."
64
-
65
- # Default prompt rendering
66
- self.prompt_service.get_system_prompt.return_value = "System prompt template: {{ user_role }}"
67
- self.utility.render_prompt_from_string.return_value = "Rendered system prompt: leader"
68
-
69
- # Default model type
70
- self.utility.is_openai_model.return_value = True
71
- self.utility.is_gemini_model.return_value = False
72
-
73
- # Create the service instance under test. This now works because the context is patched.
74
- with patch.dict(os.environ, {"LLM_MODEL": "gpt-test"}):
75
- self.service = QueryService(
76
- llm_client=self.llm_client_mock,
77
- document_service=self.document_service,
78
- document_repo=MagicMock(),
79
- llmquery_repo=self.llmquery_repo,
80
- profile_repo=self.profile_repo,
81
- prompt_service=self.prompt_service,
82
- util=self.utility,
83
- dispatcher=self.dispatcher,
84
- session_context=self.session_context
85
- )
86
-
87
- # File content for file loading tests
88
- self.document_content = b'document content'
89
- self.base64_content = base64.b64encode(self.document_content)
90
-
91
- yield # The test runs at this point
92
-
93
- # --- Input Validation Tests ---
94
-
95
- def test_llm_query_fails_if_no_company(self):
96
- """Tests that the query fails if the company does not exist."""
97
- self.profile_repo.get_company_by_short_name.return_value = None
98
- result = self.service.llm_query(company_short_name='a_company', question="test", external_user_id="test_user")
99
- assert "No existe Company ID" in result["error_message"]
100
-
101
- def test_llm_query_fails_if_no_question_or_prompt(self):
102
- """Tests that the query fails if both question and prompt_name are missing."""
103
- result = self.service.llm_query(company_short_name='a_company', external_user_id="test_user")
104
- assert "Hola, cual es tu pregunta?" in result["error_message"]
105
-
106
- def test_llm_query_fails_if_no_previous_response_id_for_openai(self):
107
- """Tests query failure if no previous response ID is found for OpenAI models."""
108
- self.session_context.get_last_response_id.return_value = None
109
- self.llm_client_mock.set_company_context.return_value = None # Simulate that context initialization also fails
110
- self.utility.is_openai_model.return_value = True
111
-
112
- result = self.service.llm_query(
113
- company_short_name='a_company',
114
- external_user_id="test_user",
115
- question="test"
116
- )
117
- assert "FATAL: No se encontró 'previous_response_id'" in result["error_message"]
118
-
119
- # --- Core Logic Tests ---
120
-
121
- def test_llm_query_with_direct_question_successfully(self):
122
- """Tests a direct query and the correct management of response IDs in the session."""
123
- self.utility.is_openai_model.return_value = True
124
-
125
- result = self.service.llm_query(company_short_name='test_company', question='hello',
126
- external_user_id='test_user')
127
-
128
- self.llm_client_mock.invoke.assert_called_once()
129
- call_args = self.llm_client_mock.invoke.call_args.kwargs
130
- assert call_args['company'] == self.company
131
- assert call_args['user_identifier'] == 'test_user'
132
- assert call_args['previous_response_id'] == 'prev_response_id'
133
- assert "La pregunta que debes responder es: hello" in call_args['context']
134
-
135
- self.session_context.save_last_response_id.assert_called_once_with(
136
- 'test_company', 'test_user', 'new_llm_response_id'
137
- )
138
- assert result['answer'] == 'LLM test response'
139
-
140
- def test_llm_query_with_prompt_name_merges_data_correctly(self):
141
- """Tests that session data is merged with request data, with the latter taking priority."""
142
- request_client_data = {'user_name': 'request_user'}
143
- external_user_id = 'ext_user_2'
144
-
145
- self.service.llm_query(
146
- company_short_name='a_company',
147
- external_user_id=external_user_id,
148
- prompt_name="analisis_cartera",
149
- client_data=request_client_data
150
- )
151
-
152
- self.llm_client_mock.invoke.assert_called_once()
153
- call_kwargs = self.llm_client_mock.invoke.call_args.kwargs
154
-
155
- expected_data = {
156
- 'prompt': 'analisis_cartera',
157
- 'data': {
158
- 'user_role': 'leader', # From session
159
- 'user_name': 'request_user', # From request (overwrites session)
160
- 'user_id': external_user_id # Added by the service
161
- }
162
- }
163
-
164
- actual_question_dict = json.loads(call_kwargs['question'])
165
- assert actual_question_dict == expected_data
166
-
167
- # --- Context Initialization Tests ---
168
-
169
- def test_llm_init_context_happy_path_external_user(self):
170
- """Tests the successful, full context initialization flow for an external user."""
171
- self.utility.is_openai_model.return_value = True
172
-
173
- self.service.llm_init_context(
174
- company_short_name='test_co',
175
- external_user_id='ext_user_123'
176
- )
177
-
178
- self.session_context.clear_all_context.assert_called_once_with(
179
- company_short_name='test_co', user_identifier='ext_user_123'
180
- )
181
- self.session_context.save_user_session_data.assert_called_once_with(
182
- 'test_co', 'ext_user_123', self.dispatcher.get_user_info.return_value
183
- )
184
- self.llm_client_mock.set_company_context.assert_called_once()
185
- self.session_context.save_last_response_id.assert_called_once_with(
186
- 'test_co', 'ext_user_123', 'new_context_response_id'
187
- )
188
-
189
- def test_llm_init_context_for_gemini_model(self):
190
- """Tests that for Gemini models, context is not sent to the LLM, but a flag is returned."""
191
- self.utility.is_openai_model.return_value = False
192
- self.utility.is_gemini_model.return_value = True
193
-
194
- response = self.service.llm_init_context(
195
- company_short_name='test_co',
196
- external_user_id='ext_user_123',
197
- model="gemini"
198
- )
199
-
200
- self.llm_client_mock.set_company_context.assert_not_called()
201
- self.session_context.save_context_history.assert_called_once()
202
- assert response == "gemini-context-initialized"
203
-
204
- def test_llm_init_context_raises_exception_if_company_not_found(self):
205
- """Tests that an exception is raised if the company does not exist during context init."""
206
- self.profile_repo.get_company_by_short_name.return_value = None
207
- with pytest.raises(IAToolkitException, match="Empresa no encontrada: invalid_co"):
208
- self.service.llm_init_context(company_short_name='invalid_co', external_user_id='user1')
209
-
210
- # --- File Loading Tests ---
211
-
212
- @patch('os.path.exists', return_value=False)
213
- def test_load_files_for_context_handles_nonexistent_file(self, mock_exists):
214
- """Tests that a non-existent file is handled gracefully."""
215
- result = self.service.load_files_for_context([{'file_id': 'nonexistent.txt'}])
216
- assert "no fue encontrado y no pudo ser cargado" in result
217
-
218
- @patch('os.path.exists', return_value=True)
219
- def test_load_files_for_context_handles_service_exception(self, mock_exists):
220
- """Tests that exceptions from the document service are caught."""
221
- self.document_service.file_to_txt.side_effect = Exception("Service failed")
222
- files = [{'file_id': 'file.pdf', 'base64': self.base64_content.decode('utf-8')}]
223
- result = self.service.load_files_for_context(files)
224
- assert "Error al procesar el archivo file.pdf" in result
225
-
226
- @patch('os.path.exists', return_value=True)
227
- def test_load_files_for_context_builds_correctly(self, mock_exists):
228
- """Tests that file context is built correctly from base64 content."""
229
- self.document_service.file_to_txt.return_value = "Text from file"
230
- files = [{'file_id': 'test.pdf', 'base64': self.base64_content.decode('utf-8')}]
231
- result = self.service.load_files_for_context(files)
232
-
233
- self.document_service.file_to_txt.assert_called_once_with('test.pdf', self.document_content)
234
- expected_context = """
235
- A continuación encontraras una lista de documentos adjuntos
236
- enviados por el usuario que hace la pregunta,
237
- en total son: 1 documentos adjuntos
238
-
239
- <document name='test.pdf'>
240
- Text from file
241
- </document>
242
- """
243
- assert result == expected_context
@@ -1,39 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- from unittest.mock import MagicMock
7
- from iatoolkit.services.search_service import SearchService
8
- from iatoolkit.repositories.models import Document
9
-
10
-
11
- class TestSearchService:
12
- def setup_method(self):
13
- # Mock de dependencias
14
- self.doc_repo = MagicMock()
15
- self.vs_repo = MagicMock()
16
-
17
- self.service = SearchService(
18
- doc_repo=self.doc_repo,
19
- vs_repo=self.vs_repo
20
- )
21
-
22
-
23
- def test_search_documents_no_results(self):
24
- self.vs_repo.query.return_value = []
25
-
26
- result = self.service.search(company_id=1, query="consulta_inexistente")
27
- assert result == ''
28
-
29
-
30
- def test_search_documents_success(self):
31
- # Mock de chunks devueltos por el vector store
32
- document = Document(id=1, company_id=1, filename='doc1.pdf', content="Contenido del documento")
33
- self.vs_repo.query.return_value = [document]
34
-
35
-
36
- # Llamar a search y verificar resultado
37
- result = self.service.search(company_id=1, query="consulta")
38
-
39
- assert result == 'documento "doc1.pdf": Contenido del documento\n'
@@ -1,160 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- import pytest
7
- from unittest.mock import MagicMock
8
- from sqlalchemy import text # Para verificar el argumento de text()
9
- import json
10
- from datetime import datetime
11
- from iatoolkit.services.sql_service import SqlService
12
- from iatoolkit.repositories.database_manager import DatabaseManager
13
- from iatoolkit.common.util import Utility
14
- from iatoolkit.common.exceptions import IAToolkitException
15
-
16
- class TestSqlService:
17
- def setup_method(self):
18
-
19
- self.db_manager_mock = MagicMock(spec=DatabaseManager)
20
- self.util_mock = MagicMock(spec=Utility)
21
-
22
- # Mock para la sesión de base de datos y el objeto de resultado de la consulta
23
- self.session_mock = MagicMock()
24
- self.mock_result_proxy = MagicMock() # Simula el objeto ResultProxy de SQLAlchemy
25
-
26
- # Configurar los mocks para que devuelvan otros mocks cuando sea necesario
27
- self.db_manager_mock.get_session.return_value = self.session_mock
28
- self.session_mock.execute.return_value = self.mock_result_proxy
29
-
30
- # Instanciar el servicio con las dependencias mockeadas
31
- self.service = SqlService(util=self.util_mock)
32
-
33
- def test_exec_sql_success_with_simple_data(self):
34
- """
35
- Prueba la ejecución exitosa de una consulta SQL que devuelve datos simples (int, str).
36
- En este caso, la función de serialización personalizada no debería ser invocada para estos tipos.
37
- """
38
- sql_statement = "SELECT id, name FROM users WHERE status = 'active'"
39
- expected_keys = ['id', 'name']
40
- # Datos que json.dumps puede manejar directamente
41
- expected_rows_from_db = [(1, 'Alice'), (2, 'Bob')]
42
- # Cómo se verán los datos después del procesamiento interno en exec_sql antes de json.dumps
43
- expected_rows_as_dicts = [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]
44
-
45
- self.mock_result_proxy.keys.return_value = expected_keys
46
- self.mock_result_proxy.fetchall.return_value = expected_rows_from_db
47
-
48
- # Ejecutar el método a probar
49
- result_json = self.service.exec_sql(self.db_manager_mock, sql_statement)
50
-
51
- # Verificar que la sesión y la ejecución fueron llamadas correctamente
52
- self.db_manager_mock.get_session.assert_called_once()
53
- self.session_mock.execute.assert_called_once()
54
- # Verificar que el argumento de execute fue el objeto text(sql_statement)
55
- args, _ = self.session_mock.execute.call_args
56
- assert isinstance(args[0], type(text(""))) # Comprueba el tipo del argumento
57
- assert str(args[0]) == sql_statement # Comprueba el contenido del SQL
58
-
59
- self.mock_result_proxy.keys.assert_called_once()
60
- self.mock_result_proxy.fetchall.assert_called_once()
61
-
62
- # Verificar que la función de serialización personalizada no fue llamada
63
- # para tipos que json.dumps maneja nativamente.
64
- self.util_mock.serialize.assert_not_called()
65
-
66
- # Verificar el resultado JSON
67
- expected_json_output = json.dumps(expected_rows_as_dicts)
68
- assert result_json == expected_json_output
69
-
70
- def test_exec_sql_success_with_custom_data_type_serialization(self):
71
- """
72
- Prueba la ejecución exitosa de una consulta SQL que devuelve datos que requieren
73
- serialización personalizada (ej. un objeto datetime).
74
- """
75
- sql_statement = "SELECT event_name, event_time FROM important_events"
76
- original_datetime_obj = datetime(2024, 1, 15, 10, 30, 0)
77
- # Suponemos que util.serialize convierte datetime a una string ISO
78
- serialized_datetime_str = original_datetime_obj.isoformat()
79
-
80
- expected_keys = ['event_name', 'event_time']
81
- # La base de datos devuelve una tupla con un objeto datetime
82
- expected_rows_from_db = [('Team Meeting', original_datetime_obj)]
83
- # El diccionario que se pasará a json.dumps, después de que serialize haga su trabajo
84
- expected_rows_as_dicts_after_serialization = [{'event_name': 'Team Meeting', 'event_time': serialized_datetime_str}]
85
-
86
-
87
- self.mock_result_proxy.keys.return_value = expected_keys
88
- self.mock_result_proxy.fetchall.return_value = expected_rows_from_db
89
-
90
- # Configurar el mock de util.serialize para que maneje datetime
91
- def mock_serializer(obj):
92
- if isinstance(obj, datetime):
93
- return obj.isoformat()
94
- # Para cualquier otro tipo, podría devolverlo tal cual si json.dumps lo maneja,
95
- # o lanzar un TypeError como lo haría json.dumps si default no estuviera.
96
- return obj
97
- self.util_mock.serialize.side_effect = mock_serializer
98
-
99
- # Ejecutar el método a probar
100
- result_json = self.service.exec_sql(self.db_manager_mock, sql_statement)
101
-
102
- # Verificar llamadas
103
- self.session_mock.execute.assert_called_once()
104
- self.mock_result_proxy.keys.assert_called_once()
105
- self.mock_result_proxy.fetchall.assert_called_once()
106
-
107
- # Verificar que util.serialize fue llamado para el objeto datetime
108
- self.util_mock.serialize.assert_called_once_with(original_datetime_obj)
109
-
110
- # Verificar el resultado JSON
111
- expected_json_output = json.dumps(expected_rows_as_dicts_after_serialization)
112
- assert result_json == expected_json_output
113
-
114
- def test_exec_sql_success_no_results(self):
115
- """
116
- Prueba la ejecución de una consulta SQL que no devuelve resultados.
117
- """
118
- sql_statement = "SELECT id FROM users WHERE name = 'NonExistentUser'"
119
- expected_keys = ['id'] # Las claves se devuelven incluso sin filas
120
- expected_rows_from_db = []
121
- expected_rows_as_dicts = []
122
-
123
- self.mock_result_proxy.keys.return_value = expected_keys
124
- self.mock_result_proxy.fetchall.return_value = expected_rows_from_db
125
-
126
- result_json = self.service.exec_sql(self.db_manager_mock, sql_statement)
127
-
128
- self.session_mock.execute.assert_called_once()
129
- self.mock_result_proxy.keys.assert_called_once()
130
- self.mock_result_proxy.fetchall.assert_called_once()
131
- self.util_mock.serialize.assert_not_called() # No hay datos que necesiten serialización
132
-
133
- expected_json_output = json.dumps(expected_rows_as_dicts, indent=2)
134
- assert result_json == expected_json_output
135
-
136
- def test_exec_sql_raises_app_exception_on_database_error(self):
137
- """
138
- Prueba que se lanza una IAToolkitException cuando ocurre un error en la base de datos.
139
- """
140
- sql_statement = "SELECT * FROM table_that_does_not_exist"
141
- original_db_error_message = "Error: Table not found"
142
- # Configurar el mock para que lance una excepción cuando se llame a execute
143
- self.session_mock.execute.side_effect = Exception(original_db_error_message)
144
-
145
- with pytest.raises(IAToolkitException) as exc_info:
146
- self.service.exec_sql(self.db_manager_mock, sql_statement)
147
-
148
- # Verificar el tipo de excepción y el mensaje
149
- assert exc_info.value.error_type == IAToolkitException.ErrorType.DATABASE_ERROR
150
- assert original_db_error_message in str(exc_info.value)
151
- # Verificar que la excepción original está encadenada (from e)
152
- assert isinstance(exc_info.value.__cause__, Exception)
153
- assert str(exc_info.value.__cause__) == original_db_error_message
154
-
155
- # Verificar que se intentó ejecutar la consulta
156
- self.session_mock.execute.assert_called_once()
157
- # Otros mocks no deberían haber sido llamados si execute falló
158
- self.mock_result_proxy.keys.assert_not_called()
159
- self.mock_result_proxy.fetchall.assert_not_called()
160
- self.util_mock.serialize.assert_not_called()