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,208 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- import os
7
- import shutil
8
- import tempfile
9
- from unittest.mock import MagicMock, patch
10
-
11
- import pytest
12
- from flask import Flask
13
-
14
- from iatoolkit.services.excel_service import ExcelService
15
- from iatoolkit.common.util import Utility
16
-
17
-
18
- class TestExcelService:
19
- @pytest.fixture(autouse=True)
20
- def setup(self):
21
- self.app = Flask(__name__)
22
- self.app.testing = True
23
-
24
- # Create real temporary directory structure
25
- self.temp_dir_base = tempfile.mkdtemp()
26
- self.app.root_path = self.temp_dir_base
27
- self.temp_dir = os.path.join(self.temp_dir_base, 'static', 'temp')
28
- os.makedirs(self.temp_dir, exist_ok=True)
29
-
30
- # Mocks of services
31
- self.util = MagicMock(spec=Utility)
32
- self.excel_service = ExcelService(util=self.util)
33
-
34
- yield
35
-
36
- # Cleanup after test
37
- shutil.rmtree(self.temp_dir_base)
38
-
39
- def create_test_file(self, filename, content=b'test content'):
40
- file_path = os.path.join(self.temp_dir, filename)
41
- with open(file_path, 'wb') as f:
42
- f.write(content)
43
- return file_path
44
-
45
- def test_validate_file_access_valid_file(self):
46
- filename = 'valid_file.xlsx'
47
- self.create_test_file(filename)
48
-
49
- with self.app.app_context():
50
- with patch('iatoolkit.services.excel_service.current_app') as mock_current_app:
51
- mock_current_app.root_path = self.temp_dir_base
52
- result = self.excel_service.validate_file_access(filename)
53
-
54
- assert result is None
55
-
56
- def test_validate_file_access_path_traversal_dotdot(self):
57
- filename = '../../../etc/passwd'
58
-
59
- with self.app.app_context():
60
- with patch('iatoolkit.services.excel_service.current_app') as mock_current_app:
61
- mock_current_app.root_path = self.temp_dir_base
62
- result = self.excel_service.validate_file_access(filename)
63
-
64
- assert result is not None
65
- data = result.get_json()
66
- assert data['error'] == 'Nombre de archivo inválido'
67
-
68
- def test_validate_file_access_path_traversal_absolute_unix(self):
69
- filename = '/etc/passwd'
70
-
71
- with self.app.app_context():
72
- with patch('iatoolkit.services.excel_service.current_app') as mock_current_app:
73
- mock_current_app.root_path = self.temp_dir_base
74
- result = self.excel_service.validate_file_access(filename)
75
-
76
- assert result is not None
77
- data = result.get_json()
78
- assert data['error'] == 'Nombre de archivo inválido'
79
-
80
- def test_validate_file_access_path_traversal_backslash(self):
81
- filename = 'folder\\..\\sensitive_file.txt'
82
-
83
- with self.app.app_context():
84
- with patch('iatoolkit.services.excel_service.current_app') as mock_current_app:
85
- mock_current_app.root_path = self.temp_dir_base
86
- result = self.excel_service.validate_file_access(filename)
87
-
88
- assert result is not None
89
- data = result.get_json()
90
- assert data['error'] == 'Nombre de archivo inválido'
91
-
92
- def test_validate_file_access_file_not_found(self):
93
- filename = 'non_existent_file.xlsx'
94
-
95
- with self.app.app_context():
96
- with patch('iatoolkit.services.excel_service.current_app') as mock_current_app:
97
- mock_current_app.root_path = self.temp_dir_base
98
- result = self.excel_service.validate_file_access(filename)
99
-
100
- assert result is not None
101
- data = result.get_json()
102
- assert data['error'] == 'Archivo no encontrado'
103
-
104
- def test_validate_file_access_is_directory(self):
105
- dirname = 'test_directory'
106
- dir_path = os.path.join(self.temp_dir, dirname)
107
- os.makedirs(dir_path)
108
-
109
- with self.app.app_context():
110
- with patch('iatoolkit.services.excel_service.current_app') as mock_current_app:
111
- mock_current_app.root_path = self.temp_dir_base
112
- result = self.excel_service.validate_file_access(dirname)
113
-
114
- assert result is not None
115
- data = result.get_json()
116
- assert data['error'] == 'La ruta no corresponde a un archivo'
117
-
118
- def test_validate_file_access_exception_handling(self):
119
- filename = 'test_file.xlsx'
120
-
121
- with self.app.app_context():
122
- with patch('iatoolkit.services.excel_service.current_app') as mock_current_app:
123
- mock_current_app.root_path = self.temp_dir_base
124
- with patch('iatoolkit.services.excel_service.os.path.exists', side_effect=Exception("Test exception")):
125
- result = self.excel_service.validate_file_access(filename)
126
-
127
- assert result is not None
128
- data = result.get_json()
129
- assert data['error'] == 'Error validando archivo'
130
-
131
- def test_validate_file_access_logs_exception(self):
132
- filename = 'test_file.xlsx'
133
-
134
- with patch('iatoolkit.services.excel_service.logging') as mock_logging:
135
- with patch('iatoolkit.services.excel_service.os.path.exists', side_effect=Exception("Test exception")):
136
- with self.app.app_context():
137
- with patch('iatoolkit.services.excel_service.current_app') as mock_current_app:
138
- mock_current_app.root_path = self.temp_dir_base
139
- self.excel_service.validate_file_access(filename)
140
-
141
- mock_logging.error.assert_called_once()
142
- error_msg = mock_logging.error.call_args[0][0]
143
- assert 'Error validando acceso al archivo test_file.xlsx' in error_msg
144
- assert 'Test exception' in error_msg
145
-
146
- def test_validate_file_access_various_valid_filenames(self):
147
- with self.app.app_context():
148
- with patch('iatoolkit.services.excel_service.current_app') as mock_current_app:
149
- mock_current_app.root_path = self.temp_dir_base
150
-
151
- valid_filenames = [
152
- 'simple.xlsx',
153
- 'file_with_underscores.xlsx',
154
- 'file-with-dashes.xlsx',
155
- 'file with spaces.xlsx',
156
- 'file123.xlsx',
157
- 'UPPERCASE.XLSX',
158
- 'file.with.dots.xlsx'
159
- ]
160
-
161
- for filename in valid_filenames:
162
- self.create_test_file(filename)
163
- result = self.excel_service.validate_file_access(filename)
164
- assert result is None, f"Filename '{filename}' should be valid"
165
-
166
- def test_validate_file_access_various_invalid_filenames(self):
167
- with self.app.app_context():
168
- with patch('iatoolkit.services.excel_service.current_app') as mock_current_app:
169
- mock_current_app.root_path = self.temp_dir_base
170
-
171
- invalid_filenames = [
172
- '../file.xlsx',
173
- '../../file.xlsx',
174
- '/absolute/path/file.xlsx',
175
- 'folder\\file.xlsx',
176
- '..\\file.xlsx',
177
- 'file..xlsx/../other.xlsx',
178
- '/etc/passwd',
179
- 'C:\\Windows\\System32\\config'
180
- ]
181
-
182
- for filename in invalid_filenames:
183
- result = self.excel_service.validate_file_access(filename)
184
- assert result is not None, f"Filename '{filename}' should be invalid"
185
- data = result.get_json()
186
- assert data['error'] == 'Nombre de archivo inválido'
187
-
188
- def test_validate_file_access_empty_filename(self):
189
- filename = ''
190
-
191
- with self.app.app_context():
192
- with patch('iatoolkit.services.excel_service.current_app') as mock_current_app:
193
- mock_current_app.root_path = self.temp_dir_base
194
- result = self.excel_service.validate_file_access(filename)
195
-
196
- assert result is not None
197
- data = result.get_json()
198
- assert data['error'] == 'Nombre de archivo inválido'
199
-
200
- def test_validate_file_access_none_filename(self):
201
- with self.app.app_context():
202
- with patch('iatoolkit.services.excel_service.current_app') as mock_current_app:
203
- mock_current_app.root_path = self.temp_dir_base
204
- result = self.excel_service.validate_file_access(None)
205
-
206
- assert result is not None
207
- data = result.get_json()
208
- assert data['error'] == 'Nombre de archivo inválido'
@@ -1,121 +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, patch
8
- from iatoolkit.services.file_processor_service import FileProcessor, FileProcessorConfig
9
- from iatoolkit.infra.connectors.file_connector import FileConnector
10
-
11
-
12
- class TestFileProcessor:
13
- def setup_method(self):
14
- # Mock del conector de archivos
15
- self.mock_connector = MagicMock(spec=FileConnector)
16
-
17
- # Acción de ejemplo para la configuración
18
- self.mock_callback = MagicMock()
19
- self.mock_company = MagicMock()
20
-
21
- # Configuración del FileProcessorConfig con validaciones y mocks
22
- self.mock_config = FileProcessorConfig(
23
- log_file="mock_log_file.log", # Archivo de log ficticio
24
- callback=self.mock_callback, # Acción mockeada
25
- filters={
26
- "filename_contains": "test",
27
- "custom_filter": lambda x: x.endswith(".txt") # Solo procesar archivos .txt
28
- },
29
- echo=True,
30
- continue_on_error=True,
31
- context={'company': self.mock_company,}
32
- )
33
-
34
- # Creamos la instancia de FileProcessor con los mocks
35
- self.processor = FileProcessor(
36
- self.mock_connector,
37
- self.mock_config
38
- )
39
-
40
- self.mock_connector.list_files.return_value = [
41
- {'path': "/mock/directory/test_file.txt",
42
- 'name': 'test_file.txt'}
43
- ]
44
-
45
- def test_process_files_when_exception_on_list_files(self):
46
- # Generar un error intencional al abrir un archivo
47
- self.mock_connector.list_files.side_effect = Exception("list file error")
48
-
49
- status = self.processor.process_files()
50
- assert status == False
51
-
52
- def test_process_files_when_exception_on_get_content(self):
53
- self.mock_connector.get_file_content.side_effect = Exception("Mocked error")
54
-
55
- # Mock del logger para capturar errores
56
- with patch.object(self.processor, "logger") as mock_logger:
57
- # Ejecutar el método y permitir que continúe tras el error
58
- self.processor.process_files()
59
-
60
- # Verificar que no se ejecutó la acción debido al error
61
- self.mock_callback.assert_not_called()
62
-
63
- # Verificar que el logger capture el error
64
- mock_logger.error.assert_called_once_with("Error processing /mock/directory/test_file.txt: Mocked error")
65
-
66
- def test_process_files_filtered_out(self):
67
- """Prueba que se filtren los archivos que no coincidan con los filtros."""
68
- self.mock_connector.list_files.return_value = [
69
- {'path': "/mock/directory/test_file1.doc",
70
- 'name': 'test_file1.doc'},
71
- {'path': "/mock/directory/test_file2.txt",
72
- 'name': 'test_file2.txt'}
73
- ]
74
-
75
- self.mock_connector.get_file_content.return_value = b"content of file2"
76
-
77
- self.processor.process_files()
78
-
79
- # Solo debe procesarse test_file2.txt
80
- self.mock_callback.assert_called_once_with(
81
- company=self.mock_company,
82
- filename="test_file2.txt",
83
- content=b"content of file2",
84
- context={'company': self.mock_company})
85
- assert self.mock_callback.call_count == 1
86
- assert self.mock_connector.get_file_content.call_count == 1
87
-
88
- def test_process_files_stop_on_error(self):
89
- """Prueba que se detenga el procesamiento si `continue_on_error` es False."""
90
- self.mock_config.continue_on_error = False # Configuración para detenerse en errores
91
- self.mock_connector.get_file_content.side_effect = Exception("Mocked error")
92
-
93
- # Verificar que se lanza la excepción y no continúa procesando
94
- with pytest.raises(Exception, match="Mocked error"):
95
- self.processor.process_files()
96
-
97
- # No se debe haber realizado ninguna acción
98
- self.mock_callback.assert_not_called()
99
-
100
- def test_process_files_success(self):
101
- self.mock_connector.list_files.return_value = [
102
- {'path': "/mock/directory/test_file1.txt",
103
- 'name': 'test_file1.txt'},
104
- {'path': "/mock/directory/test_file2.txt",
105
- 'name': 'test_file2.txt'}
106
- ]
107
- self.mock_connector.get_file_content.side_effect = [
108
- b"content of file1",
109
- b"content of file2",
110
- ]
111
-
112
- self.processor.process_files()
113
-
114
- # Verificar que se llamaron las acciones con los archivos correctos
115
- assert self.mock_callback.call_count == 2 # Solo archivos filtrados
116
-
117
- # Verificar numero de archivos procesados
118
- assert self.processor.processed_files == 2
119
-
120
- # Verificar las llamadas al logger (si es necesario)
121
- self.mock_connector.list_files.assert_called_once()
@@ -1,164 +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 iatoolkit.services.history_service import HistoryService
9
- from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
10
- from iatoolkit.repositories.profile_repo import ProfileRepo
11
- from iatoolkit.repositories.models import LLMQuery, Company
12
- from iatoolkit.common.util import Utility
13
- from iatoolkit.common.session_manager import SessionManager
14
- from unittest.mock import patch
15
-
16
-
17
- class TestHistoryService:
18
- @pytest.fixture(autouse=True)
19
- def setup(self):
20
- self.llm_query_repo = MagicMock(spec=LLMQueryRepo)
21
- self.profile_repo = MagicMock(spec=ProfileRepo)
22
- # Usamos la clase Utility real para asegurar que la lógica de resolución de ID es correcta
23
- self.util = Utility()
24
- self.history_service = HistoryService(
25
- llm_query_repo=self.llm_query_repo,
26
- profile_repo=self.profile_repo,
27
- util=self.util
28
- )
29
- # Mock común para la compañía
30
- self.mock_company = MagicMock(spec=Company)
31
- self.mock_company.id = 1
32
- self.mock_company.name = 'Test Company'
33
- self.profile_repo.get_company_by_short_name.return_value = self.mock_company
34
-
35
- def test_get_history_company_not_found(self):
36
- """Prueba que el servicio devuelve un error si la empresa no se encuentra."""
37
- self.profile_repo.get_company_by_short_name.return_value = None
38
-
39
- result = self.history_service.get_history(
40
- company_short_name='nonexistent_company',
41
- external_user_id='test_user'
42
- )
43
-
44
- assert result == {'error': 'No existe la empresa: nonexistent_company'}
45
- self.profile_repo.get_company_by_short_name.assert_called_once_with('nonexistent_company')
46
- self.llm_query_repo.get_history.assert_not_called()
47
-
48
- def test_get_history_no_history_found(self):
49
- """Prueba que el servicio devuelve un error si no se encuentra historial."""
50
- self.llm_query_repo.get_history.return_value = []
51
- user_identifier = 'test_user'
52
-
53
- result = self.history_service.get_history(
54
- company_short_name='test_company',
55
- external_user_id=user_identifier
56
- )
57
-
58
- assert result == {'error': 'No se pudo obtener el historial'}
59
- self.profile_repo.get_company_by_short_name.assert_called_once_with('test_company')
60
- self.llm_query_repo.get_history.assert_called_once_with(self.mock_company, user_identifier)
61
-
62
- def test_get_history_success_with_external_user_id(self):
63
- """Prueba la recuperación exitosa del historial usando un external_user_id."""
64
- user_identifier = 'external_user_123'
65
-
66
- mock_query1 = MagicMock(spec=LLMQuery)
67
- mock_query1.to_dict.return_value = {'id': 1, 'query': 'q1', 'answer': 'a1', 'created_at': 't1'}
68
- mock_query2 = MagicMock(spec=LLMQuery)
69
- mock_query2.to_dict.return_value = {'id': 2, 'query': 'q2', 'answer': 'a2', 'created_at': 't2'}
70
-
71
- self.llm_query_repo.get_history.return_value = [mock_query1, mock_query2]
72
-
73
- result = self.history_service.get_history(
74
- company_short_name='test_company',
75
- external_user_id=user_identifier
76
- )
77
-
78
- assert result['message'] == 'Historial obtenido correctamente'
79
- assert len(result['history']) == 2
80
- assert result['history'][0]['id'] == 1
81
-
82
- self.llm_query_repo.get_history.assert_called_once_with(self.mock_company, user_identifier)
83
-
84
- def test_get_history_success_with_local_user_id_and_external_id_takes_precedence(self):
85
- """Prueba que external_user_id tiene prioridad sobre local_user_id."""
86
- external_user_identifier = 'external_user_abc'
87
-
88
- mock_query = MagicMock(spec=LLMQuery)
89
- mock_query.to_dict.return_value = {'id': 1}
90
- self.llm_query_repo.get_history.return_value = [mock_query]
91
-
92
- result = self.history_service.get_history(
93
- company_short_name='test_company',
94
- external_user_id=external_user_identifier,
95
- local_user_id=123 # Este debería ser ignorado
96
- )
97
-
98
- assert 'history' in result
99
- # El user_identifier resuelto debe ser el ID externo.
100
- self.llm_query_repo.get_history.assert_called_once_with(self.mock_company, external_user_identifier)
101
-
102
- def test_get_history_success_with_local_user_id_only(self):
103
- local_id = 456
104
- resolved_user_identifier = 'fl@gmail.com'
105
-
106
- with patch('iatoolkit.common.session_manager.session', new={}) as fake_session:
107
- SessionManager.set('user', {'id': local_id, 'email': resolved_user_identifier})
108
-
109
- mock_query = MagicMock(spec=LLMQuery)
110
- mock_query.to_dict.return_value = {'id': 1, 'query': 'q1'}
111
- self.llm_query_repo.get_history.return_value = [mock_query]
112
-
113
- result = self.history_service.get_history(
114
- company_short_name='test_company',
115
- local_user_id=local_id
116
- )
117
-
118
- assert result['message'] == 'Historial obtenido correctamente'
119
- self.llm_query_repo.get_history.assert_called_once_with(self.mock_company, resolved_user_identifier)
120
-
121
- def test_get_history_fails_when_no_user_identifier_is_provided(self):
122
- """Prueba que falla si no se proporciona ningún identificador de usuario."""
123
- result = self.history_service.get_history(company_short_name='test_company')
124
-
125
- assert result == {'error': 'No se pudo resolver el identificador del usuario'}
126
- self.llm_query_repo.get_history.assert_not_called()
127
-
128
- def test_get_history_propagates_exception_from_company_lookup(self):
129
- """Prueba que las excepciones de la capa de repositorio se propagan."""
130
- self.profile_repo.get_company_by_short_name.side_effect = Exception('Database error')
131
-
132
- result = self.history_service.get_history(
133
- company_short_name='test_company',
134
- external_user_id='test_user'
135
- )
136
-
137
- assert result == {'error': 'Database error'}
138
- self.llm_query_repo.get_history.assert_not_called()
139
-
140
- def test_get_history_propagates_exception_from_history_lookup(self):
141
- """Prueba que las excepciones de la capa de repositorio se propagan."""
142
- user_identifier = 'test_user'
143
- self.llm_query_repo.get_history.side_effect = Exception('History lookup error')
144
-
145
- result = self.history_service.get_history(
146
- company_short_name='test_company',
147
- external_user_id=user_identifier
148
- )
149
-
150
- assert result == {'error': 'History lookup error'}
151
- self.llm_query_repo.get_history.assert_called_once_with(self.mock_company, user_identifier)
152
-
153
- def test_get_history_none_returned_from_repo(self):
154
- """Prueba el caso en que el repositorio devuelve None."""
155
- self.llm_query_repo.get_history.return_value = None
156
- user_identifier = 'test_user'
157
-
158
- result = self.history_service.get_history(
159
- company_short_name='test_company',
160
- external_user_id=user_identifier
161
- )
162
-
163
- assert result == {'error': 'No se pudo obtener el historial'}
164
- self.llm_query_repo.get_history.assert_called_once_with(self.mock_company, user_identifier)