totelegram 0.9.10__tar.gz

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.
Files changed (45) hide show
  1. totelegram-0.9.10/PKG-INFO +70 -0
  2. totelegram-0.9.10/README.md +45 -0
  3. totelegram-0.9.10/setup.cfg +4 -0
  4. totelegram-0.9.10/setup.py +34 -0
  5. totelegram-0.9.10/tests/test_adr.py +42 -0
  6. totelegram-0.9.10/tests/test_chunking.py +21 -0
  7. totelegram-0.9.10/tests/test_command_config.py +116 -0
  8. totelegram-0.9.10/tests/test_command_profile.py +134 -0
  9. totelegram-0.9.10/tests/test_models.py +141 -0
  10. totelegram-0.9.10/tests/test_snapshot.py +100 -0
  11. totelegram-0.9.10/tests/test_stream.py +103 -0
  12. totelegram-0.9.10/totelegram/__init__.py +13 -0
  13. totelegram-0.9.10/totelegram/cli/__init__.py +0 -0
  14. totelegram-0.9.10/totelegram/cli/__main__.py +108 -0
  15. totelegram-0.9.10/totelegram/cli/commands/__init__.py +0 -0
  16. totelegram-0.9.10/totelegram/cli/commands/backup.py +107 -0
  17. totelegram-0.9.10/totelegram/cli/commands/config.py +343 -0
  18. totelegram-0.9.10/totelegram/cli/commands/profile.py +401 -0
  19. totelegram-0.9.10/totelegram/cli/commands/send.py +99 -0
  20. totelegram-0.9.10/totelegram/cli/logic.py +208 -0
  21. totelegram-0.9.10/totelegram/cli/ui.py +654 -0
  22. totelegram-0.9.10/totelegram/database.py +152 -0
  23. totelegram-0.9.10/totelegram/discovery.py +153 -0
  24. totelegram-0.9.10/totelegram/identity.py +599 -0
  25. totelegram-0.9.10/totelegram/logging_config.py +69 -0
  26. totelegram-0.9.10/totelegram/migration.py +73 -0
  27. totelegram-0.9.10/totelegram/models.py +527 -0
  28. totelegram-0.9.10/totelegram/packaging.py +339 -0
  29. totelegram-0.9.10/totelegram/schemas.py +326 -0
  30. totelegram-0.9.10/totelegram/stream.py +157 -0
  31. totelegram-0.9.10/totelegram/telegram/__init__.py +0 -0
  32. totelegram-0.9.10/totelegram/telegram/access.py +226 -0
  33. totelegram-0.9.10/totelegram/telegram/auth.py +108 -0
  34. totelegram-0.9.10/totelegram/telegram/client.py +202 -0
  35. totelegram-0.9.10/totelegram/telegram/patches.py +373 -0
  36. totelegram-0.9.10/totelegram/telegram/search.py +90 -0
  37. totelegram-0.9.10/totelegram/types.py +41 -0
  38. totelegram-0.9.10/totelegram/uploader.py +314 -0
  39. totelegram-0.9.10/totelegram/utils.py +389 -0
  40. totelegram-0.9.10/totelegram.egg-info/PKG-INFO +70 -0
  41. totelegram-0.9.10/totelegram.egg-info/SOURCES.txt +43 -0
  42. totelegram-0.9.10/totelegram.egg-info/dependency_links.txt +1 -0
  43. totelegram-0.9.10/totelegram.egg-info/entry_points.txt +2 -0
  44. totelegram-0.9.10/totelegram.egg-info/requires.txt +9 -0
  45. totelegram-0.9.10/totelegram.egg-info/top_level.txt +1 -0
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: totelegram
3
+ Version: 0.9.10
4
+ Summary: toTelegram sube archivos a telegram sin importar el tamaño.
5
+ Home-page: https://github.com/CalumRakk/toTelegram
6
+ Author: Leo
7
+ Requires-Python: >=3.10.0
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: filetype==1.2.0
10
+ Requires-Dist: peewee>=3.17.9
11
+ Requires-Dist: pydantic>=2.11.7
12
+ Requires-Dist: pydantic-settings>=2.10.1
13
+ Requires-Dist: pyrogram==2.0.106
14
+ Requires-Dist: TgCrypto>=1.2.5
15
+ Requires-Dist: typer>=0.21.1
16
+ Requires-Dist: filelock>=3.20.3
17
+ Requires-Dist: tartape>=2.2.0
18
+ Dynamic: author
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: requires-dist
23
+ Dynamic: requires-python
24
+ Dynamic: summary
25
+
26
+ ## toTelegram
27
+
28
+ Este proyecto es una herramienta de CLI en Python diseñada para subir archivos a Telegram sin preocuparse por los límites de tamaño (2GB/4GB). La idea no es solo tener un uploader, sino un gestor de disponibilidad que entiende qué archivos ya están en la "nube" de tus chats para evitar subidas redundantes.
29
+
30
+
31
+ ### ¿De qué va esto?
32
+ A diferencia de un uploader simple, `toTelegram` usa una base de datos local (SQLite) para trackear los archivos por su MD5. Si intentas subir algo que el sistema ya detectó en otro chat al que tienes acceso, intentará hacer un forward (mirror) o reconstruir el archivo desde piezas sueltas (puzzle) en lugar de gastar ancho de banda volviendo a subir los bytes.
33
+
34
+
35
+ ---
36
+
37
+ ### Cómo probarlo
38
+
39
+ Puedes instalar el paquete directamente desde el repositorio sin necesidad de clonarlo manualmente:
40
+
41
+
42
+ **1. Instala usando pip desde GitHub:**
43
+
44
+ ```
45
+ pip install totelegram
46
+ ```
47
+
48
+ **2. Crea un perfil:**
49
+
50
+ Necesitas tu **API_ID** y **API_HASH** de [Telegram](https://my.telegram.org/).
51
+
52
+ ```
53
+ totelegram profile create
54
+ ```
55
+
56
+ **3. Configura un chat de destino**
57
+
58
+ ```
59
+ totelegram config search "Mi Nube Privada" --apply
60
+ ```
61
+
62
+ **4. Envía archivos o carpetas:**
63
+
64
+ ```
65
+ # Enviar archivos individuales
66
+ totelegram send video.mp4
67
+
68
+ # Archivar una carpeta completa como un backup estructurado
69
+ totelegram backup ./mis_fotos
70
+ ```
@@ -0,0 +1,45 @@
1
+ ## toTelegram
2
+
3
+ Este proyecto es una herramienta de CLI en Python diseñada para subir archivos a Telegram sin preocuparse por los límites de tamaño (2GB/4GB). La idea no es solo tener un uploader, sino un gestor de disponibilidad que entiende qué archivos ya están en la "nube" de tus chats para evitar subidas redundantes.
4
+
5
+
6
+ ### ¿De qué va esto?
7
+ A diferencia de un uploader simple, `toTelegram` usa una base de datos local (SQLite) para trackear los archivos por su MD5. Si intentas subir algo que el sistema ya detectó en otro chat al que tienes acceso, intentará hacer un forward (mirror) o reconstruir el archivo desde piezas sueltas (puzzle) en lugar de gastar ancho de banda volviendo a subir los bytes.
8
+
9
+
10
+ ---
11
+
12
+ ### Cómo probarlo
13
+
14
+ Puedes instalar el paquete directamente desde el repositorio sin necesidad de clonarlo manualmente:
15
+
16
+
17
+ **1. Instala usando pip desde GitHub:**
18
+
19
+ ```
20
+ pip install totelegram
21
+ ```
22
+
23
+ **2. Crea un perfil:**
24
+
25
+ Necesitas tu **API_ID** y **API_HASH** de [Telegram](https://my.telegram.org/).
26
+
27
+ ```
28
+ totelegram profile create
29
+ ```
30
+
31
+ **3. Configura un chat de destino**
32
+
33
+ ```
34
+ totelegram config search "Mi Nube Privada" --apply
35
+ ```
36
+
37
+ **4. Envía archivos o carpetas:**
38
+
39
+ ```
40
+ # Enviar archivos individuales
41
+ totelegram send video.mp4
42
+
43
+ # Archivar una carpeta completa como un backup estructurado
44
+ totelegram backup ./mis_fotos
45
+ ```
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,34 @@
1
+ from pathlib import Path
2
+
3
+ from setuptools import find_packages, setup
4
+
5
+ from totelegram import __version__
6
+
7
+ this_directory = Path(__file__).parent
8
+ long_description = (this_directory / "README.md").read_text(encoding="utf-8")
9
+
10
+ setup(
11
+ name="totelegram",
12
+ version=__version__,
13
+ description="toTelegram sube archivos a telegram sin importar el tamaño.",
14
+ author="Leo",
15
+ url="https://github.com/CalumRakk/toTelegram",
16
+ long_description=long_description,
17
+ long_description_content_type="text/markdown",
18
+ install_requires=[
19
+ "filetype==1.2.0",
20
+ "peewee>=3.17.9",
21
+ "pydantic>=2.11.7",
22
+ "pydantic-settings>=2.10.1",
23
+ "pyrogram==2.0.106",
24
+ "TgCrypto>=1.2.5",
25
+ "typer>=0.21.1",
26
+ "filelock>=3.20.3",
27
+ "tartape>=2.2.0",
28
+ ],
29
+ packages=find_packages(),
30
+ entry_points={
31
+ "console_scripts": ["totelegram=totelegram.cli.__main__:run_script"],
32
+ },
33
+ python_requires=">=3.10.0",
34
+ )
@@ -0,0 +1,42 @@
1
+ import unittest
2
+
3
+ from totelegram.identity import Settings
4
+ from totelegram.schemas import AccessLevel
5
+
6
+
7
+ class TestArchitectureRules(unittest.TestCase):
8
+ def test_adr_001_immutable_profile_credentials(self):
9
+ """
10
+ ADR-001: Perfil Inmutable.
11
+
12
+ Las credenciales base (API_ID, API_HASH) y el nombre del perfil
13
+ DEBEN ser inmutables para garantizar la integridad de la sesión de Pyrogram.
14
+
15
+ Si este test falla, lee: docs/adr/001-perfil-inmutable.md
16
+ """
17
+ required_internal_fields = {"API_ID", "API_HASH", "PROFILE_NAME"}
18
+
19
+ api_id = Settings.get_info("API_ID")
20
+ api_hash = Settings.get_info("API_HASH")
21
+ profile_name = Settings.get_info("PROFILE_NAME")
22
+
23
+ error_message = (
24
+ f"\n\n[VIOLACIÓN DE ARQUITECTURA - ADR-001]\n"
25
+ f"Se intentó hacer editables campos críticos: {required_internal_fields}\n"
26
+ f"Estos campos deben permanecer en Settings.INTERNAL_FIELDS.\n"
27
+ f"Consulta 'docs/adr/001-perfil-inmutable.md' para más detalles.\n"
28
+ )
29
+
30
+ self.assertTrue(
31
+ api_id is not None and api_id.level == AccessLevel.DEBUG_READONLY,
32
+ error_message,
33
+ )
34
+ self.assertTrue(
35
+ api_hash is not None and api_hash.level == AccessLevel.DEBUG_READONLY,
36
+ error_message,
37
+ )
38
+ self.assertTrue(
39
+ profile_name is not None
40
+ and profile_name.level == AccessLevel.DEBUG_READONLY,
41
+ error_message,
42
+ )
@@ -0,0 +1,21 @@
1
+ import unittest
2
+
3
+ from totelegram.packaging import chunk_ranges
4
+
5
+
6
+ class TestChunkingMath(unittest.TestCase):
7
+ def test_chunk_ranges_exact_division(self):
8
+ """Caso: 10MB archivo, 5MB chunk -> 2 partes exactas"""
9
+ file_size = 10 * 1024 * 1024
10
+ chunk_size = 5 * 1024 * 1024
11
+ ranges = chunk_ranges(file_size, chunk_size)
12
+
13
+ self.assertEqual(len(ranges), 2)
14
+ self.assertEqual(ranges[0], (0, 5242880))
15
+ self.assertEqual(ranges[1], (5242880, 10485760))
16
+
17
+ def test_chunk_ranges_remainder(self):
18
+ """Caso: 10 bytes archivo, 3 bytes chunk -> 4 partes (3, 3, 3, 1)"""
19
+ ranges = chunk_ranges(10, 3)
20
+ expected = [(0, 3), (3, 6), (6, 9), (9, 10)]
21
+ self.assertEqual(ranges, expected)
@@ -0,0 +1,116 @@
1
+ import unittest
2
+ from pathlib import Path
3
+ from tempfile import TemporaryDirectory
4
+
5
+ from typer.testing import CliRunner
6
+
7
+ from totelegram.cli.commands.config import app
8
+ from totelegram.identity import SettingsManager
9
+ from totelegram.schemas import CLIState
10
+
11
+ runner = CliRunner()
12
+
13
+
14
+ class TestCliConfig(unittest.TestCase):
15
+ def setUp(self):
16
+ self.tmp_dir = TemporaryDirectory()
17
+ self.config_path = Path(self.tmp_dir.name)
18
+
19
+ self.profile_name = "test_user"
20
+ manager = SettingsManager(self.config_path)
21
+ manager._write_all_settings(
22
+ "test_user",
23
+ {
24
+ "profile_name": self.profile_name,
25
+ "chat_id": "123456789",
26
+ "upload_limit_rate_kbps": "1000",
27
+ "api_id": "123456789",
28
+ "api_hash": "123456789",
29
+ },
30
+ )
31
+
32
+ self.state = CLIState(manager=manager, profile_name=self.profile_name)
33
+ self.manager = manager
34
+
35
+ def tearDown(self):
36
+ try:
37
+ self.tmp_dir.cleanup()
38
+ except Exception:
39
+ pass
40
+
41
+ def test_config_display_table(self):
42
+ """Verificar que el comando base muestra la tabla de configuración."""
43
+ result = runner.invoke(app, obj=self.state)
44
+ self.assertEqual(result.exit_code, 0)
45
+ self.assertIn("Configuración", result.stdout)
46
+ self.assertIn("test_user", result.stdout)
47
+ self.assertIn("chat_id", result.stdout)
48
+
49
+ def test_set_valid_value(self):
50
+ """Probar que un cambio válido se persiste correctamente."""
51
+
52
+ result = runner.invoke(
53
+ app,
54
+ ["set", "upload_limit_rate_kbps", "500"],
55
+ obj=self.state,
56
+ )
57
+
58
+ self.assertEqual(result.exit_code, 0)
59
+ settings = self.manager.get_settings(self.profile_name)
60
+ current_value = getattr(settings, "upload_limit_rate_kbps", None)
61
+ self.assertEqual(current_value, 500)
62
+
63
+ def test_set_invalid_type_safety(self):
64
+ """
65
+ Si meto basura en un campo numérico,
66
+ Pydantic debe saltar y el comando debe fallar.
67
+ """
68
+
69
+ result = runner.invoke(
70
+ app, ["set", "MAX_FILESIZE_BYTES", "no_soy_un_numero"], obj=self.state
71
+ )
72
+
73
+ self.assertEqual(result.exit_code, 1)
74
+ self.assertIn("debe ser de tipo", result.stdout)
75
+
76
+ # El valor original no debería haber cambiado
77
+ settings = self.manager.get_settings(self.profile_name)
78
+ current_value = getattr(settings, "MAX_FILESIZE_BYTES", None)
79
+ self.assertNotEqual(current_value, "no_soy_un_numero")
80
+
81
+ def test_set_internal_field_protection(self):
82
+ """
83
+ ADR-001: API_ID y otros campos internos son inmutables tras la creación.
84
+ """
85
+ result = runner.invoke(app, ["set", "api_id", "99999"], obj=self.state)
86
+
87
+ self.assertIn("Solo Lectura", result.stdout.replace("\n", ""))
88
+
89
+ def test_list_add(self):
90
+ """Proba añadir elementos de una lista."""
91
+ # Añadir (confirmando con 'y')
92
+ runner.invoke(app, ["add", "exclude_files", "*.tmp", "*.bak"], obj=self.state)
93
+
94
+ settings = self.manager.get_settings(self.profile_name)
95
+ self.assertIn("*.tmp", settings.exclude_files)
96
+ self.assertIn("*.bak", settings.exclude_files)
97
+
98
+ def test_list_add_remove_flow(self):
99
+ """Probar el flujo de añadir y quitar elementos de una lista (exclude_files)."""
100
+ # Añadir (confirmando con 'y')
101
+ runner.invoke(
102
+ app, ["add", "exclude_files", "*.tmp", "*.bak"], input="y\n", obj=self.state
103
+ )
104
+
105
+ settings = self.manager.get_settings(self.profile_name)
106
+ self.assertIn("*.tmp", settings.exclude_files) # type: ignore
107
+ self.assertIn("*.bak", settings.exclude_files) # type: ignore
108
+
109
+ # Quitar uno
110
+ runner.invoke(
111
+ app, ["remove", "exclude_files", "*.tmp"], input="y\n", obj=self.state
112
+ )
113
+
114
+ settings = self.manager.get_settings(self.profile_name)
115
+ self.assertNotIn("*.tmp", settings.exclude_files) # type: ignore
116
+ self.assertIn("*.bak", settings.exclude_files) # type: ignore
@@ -0,0 +1,134 @@
1
+ import shutil
2
+ import tempfile
3
+ import unittest
4
+ from pathlib import Path
5
+ from unittest.mock import patch
6
+
7
+ from typer.testing import CliRunner
8
+
9
+ from totelegram.cli.commands.profile import app
10
+ from totelegram.identity import SettingsManager
11
+ from totelegram.schemas import AccessReport, AccessStatus, ChatMatch, CLIState
12
+
13
+
14
+ class TestCliProfile(unittest.TestCase):
15
+ def setUp(self):
16
+ """Configuración inicial antes de cada test."""
17
+ self.runner = CliRunner()
18
+ self.test_dir = tempfile.mkdtemp()
19
+ self.workdir = Path(self.test_dir)
20
+
21
+ self.manager = SettingsManager(self.workdir)
22
+ self.manager.profiles_dir.mkdir(parents=True, exist_ok=True)
23
+ self.manager.inventories_dir.mkdir(parents=True, exist_ok=True)
24
+
25
+ self.state = CLIState(manager=self.manager, profile_name=None, is_debug=False)
26
+
27
+ def tearDown(self):
28
+ """Limpieza después de cada test."""
29
+ shutil.rmtree(self.test_dir)
30
+
31
+ def test_list_profiles_empty(self):
32
+ """Debe informar que no hay perfiles si la carpeta está vacía."""
33
+ result = self.runner.invoke(app, ["list"], obj=self.state)
34
+ self.assertEqual(result.exit_code, 0)
35
+ self.assertIn("No se encontraron perfiles", result.stdout)
36
+
37
+ @patch("totelegram.telegram.auth.AuthLogic.initialize_profile")
38
+ @patch("totelegram.telegram.client.TelegramSession.from_profile")
39
+ @patch("totelegram.telegram.access.ChatAccessService.verify_access")
40
+ def test_create_profile_success_with_chat_id(
41
+ self, mock_verify, mock_session, mock_auth
42
+ ):
43
+ """Prueba la creación de un perfil con chat_id directo (sin wizard)."""
44
+
45
+ profile_name = "test_profile"
46
+
47
+ # Simulamos que el acceso al chat es válido
48
+ mock_verify.return_value = AccessReport(
49
+ status=AccessStatus.READY,
50
+ chat=ChatMatch(id=-100123456, title="Destino Test", type="channel"),
51
+ reason="Acceso verificado",
52
+ )
53
+
54
+ # Ejecutamos el comando pasando argumentos para evitar prompts interactivos
55
+ result = self.runner.invoke(
56
+ app,
57
+ [
58
+ "create",
59
+ "--profile-name",
60
+ profile_name,
61
+ "--api-id",
62
+ "123456",
63
+ "--api-hash",
64
+ "abcdef",
65
+ "--chat-id",
66
+ "-100123456",
67
+ ],
68
+ obj=self.state,
69
+ )
70
+
71
+ self.assertEqual(result.exit_code, 0)
72
+ self.assertIn(f"Creando perfil", result.stdout)
73
+
74
+ # Verificamos que se guardó la configuración en el .env
75
+ settings = self.manager._load_and_sanitize(profile_name)
76
+ self.assertEqual(settings["chat_id"], -100123456)
77
+
78
+ def test_switch_profile_fails_if_incomplete(self):
79
+ """No debe permitir cambiar a un perfil que no sea 'trinity' (incompleto)."""
80
+ profile_name = "orphan"
81
+
82
+ # Creamos solo el .env, pero no el archivo .session
83
+ env_path = self.manager.get_settings_path(profile_name)
84
+ env_path.parent.mkdir(parents=True, exist_ok=True)
85
+ env_path.write_text("profile_name=orphan")
86
+
87
+ result = self.runner.invoke(app, ["switch", profile_name], obj=self.state)
88
+
89
+ self.assertEqual(result.exit_code, 1)
90
+ self.assertIn("está incompleto", result.stdout)
91
+
92
+ def test_switch_profile_success(self):
93
+ """Cambio exitoso de perfil cuando existen .env y .session."""
94
+ profile_name = "active_user"
95
+
96
+ # 1. Crear .env
97
+ env_path = self.manager.get_settings_path(profile_name)
98
+ env_path.parent.mkdir(parents=True, exist_ok=True)
99
+ env_path.write_text("profile_name=active_user\nchat_id=12345")
100
+
101
+ # 2. Crear .session (archivo vacío para simular existencia)
102
+ session_path = self.manager.get_session_path(profile_name)
103
+ session_path.write_text("fake session data")
104
+
105
+ result = self.runner.invoke(app, ["switch", profile_name], obj=self.state)
106
+
107
+ self.assertEqual(result.exit_code, 0)
108
+ self.assertIn("Perfil cambiado exitosamente", result.stdout)
109
+ self.assertEqual(self.manager.get_active_profile_name(), profile_name)
110
+
111
+ def test_delete_profile_full(self):
112
+ """Eliminación física de un perfil."""
113
+ profile_name = "target_delete"
114
+
115
+ # Crear rastro
116
+ env_path = self.manager.get_settings_path(profile_name)
117
+ env_path.parent.mkdir(parents=True, exist_ok=True)
118
+ env_path.write_text("data")
119
+ session_path = self.manager.get_session_path(profile_name)
120
+ session_path.write_text("data")
121
+
122
+ # Usamos --yes para saltar la confirmación interactiva
123
+ result = self.runner.invoke(
124
+ app, ["delete", profile_name, "--yes"], obj=self.state
125
+ )
126
+
127
+ self.assertEqual(result.exit_code, 0)
128
+ self.assertFalse(env_path.exists())
129
+ self.assertFalse(session_path.exists())
130
+ self.assertIn(f"eliminado correctamente", result.stdout)
131
+
132
+
133
+ if __name__ == "__main__":
134
+ unittest.main()
@@ -0,0 +1,141 @@
1
+ import unittest
2
+
3
+ import peewee
4
+
5
+ from totelegram.database import DatabaseSession # type: ignore
6
+ from totelegram.models import (
7
+ Job,
8
+ Source,
9
+ TelegramChat,
10
+ )
11
+ from totelegram.schemas import Strategy
12
+
13
+
14
+ class TestModelsArchitecture(unittest.TestCase):
15
+ def setUp(self):
16
+ self.db_manager = DatabaseSession(":memory:")
17
+ self.db_manager.start()
18
+ self.chat = TelegramChat.create(
19
+ id=-100123456, title="Test Chat", type="channel"
20
+ )
21
+ self.chat_alternate = TelegramChat.create(
22
+ id=-100123801, title="Test Chat", type="channel"
23
+ )
24
+
25
+ def tearDown(self):
26
+ self.db_manager.close()
27
+
28
+ def test_source_file_uniqueness_by_md5(self):
29
+ """Prueba la unicidad de Source por MD5."""
30
+ Source.create(
31
+ path_str="video.mp4",
32
+ md5sum="abc123",
33
+ size=500,
34
+ mtime=1.0,
35
+ mimetype="video/mp4",
36
+ )
37
+ with self.assertRaises(peewee.IntegrityError):
38
+ Source.create(
39
+ path_str="otra_ruta/video.mp4",
40
+ md5sum="abc123",
41
+ size=500,
42
+ mtime=1.0,
43
+ mimetype="video/mp4",
44
+ )
45
+
46
+ def test_job_contract_strategy_assignment(self):
47
+ """
48
+ Prueba la lógica de ADR-002:
49
+ El Job determina la estrategia al nacer basado en el límite de bytes.
50
+ """
51
+ source = Source.create(
52
+ path_str="data.bin",
53
+ md5sum="hash1",
54
+ size=150,
55
+ mtime=1.0,
56
+ mimetype="application/octet-stream",
57
+ )
58
+
59
+ # CASO : El archivo (150b) es mayor que el limite (100b) -> CHUNKED
60
+
61
+ job_chunked = Job.formalize_intent(
62
+ source, self.chat, is_premium=False, tg_limit=100
63
+ )
64
+ self.assertEqual(job_chunked.strategy, Strategy.CHUNKED)
65
+ self.assertEqual(job_chunked.config.tg_max_size, 100)
66
+
67
+ # CASO : El archivo (150b) es menor que el limite (200b) -> SINGLE
68
+ job_single = Job.formalize_intent(
69
+ source, self.chat_alternate, is_premium=True, tg_limit=200
70
+ )
71
+ self.assertEqual(job_single.strategy, Strategy.SINGLE)
72
+ self.assertEqual(job_single.config.tg_max_size, 200)
73
+
74
+ def test_job_immutability_integrity(self):
75
+ """
76
+ Verifica que una vez creado el Job, su config queda persistida en JSON
77
+ y no cambia aunque cambien los parámetros externos (ADR-002).
78
+ """
79
+ source = Source.create(
80
+ path_str="test.zip",
81
+ md5sum="hash_imm",
82
+ size=150,
83
+ mtime=1.0,
84
+ mimetype="app/zip",
85
+ )
86
+
87
+ job = Job.formalize_intent(source, self.chat, is_premium=False, tg_limit=100)
88
+
89
+ # Recuperamos de la DB para asegurar que el JSONField funcionó
90
+ job_from_db = Job.get_by_id(job.id)
91
+ self.assertEqual(job_from_db.config.tg_max_size, 100)
92
+
93
+ # def test_payload_relation_and_status(self):
94
+ # """Valida que los payloads se vinculen correctamente y el Job cambie de estado."""
95
+ # source = Source.create(
96
+ # path_str="doc.pdf", md5sum="h_pdf", size=10, mtime=1.0, mimetype="app/pdf"
97
+ # )
98
+
99
+ # job = Job.formalize_intent(source, self.chat, is_premium=False, tg_limit=100)
100
+
101
+ # self.assertEqual(job.status, JobStatus.PENDING)
102
+
103
+ # # Simular creación de un payload (SINGLE)
104
+ # Payload.create_payloads(job, [Path("doc.pdf")])
105
+ # self.assertEqual(job.payloads.count(), 1)
106
+
107
+ # job.set_uploaded()
108
+ # self.assertEqual(job.status, JobStatus.UPLOADED)
109
+
110
+ # def test_remote_payload_register_upload(self):
111
+ # """Valida el registro del 'Vínculo de Acceso' (RemotePayload)."""
112
+ # source = Source.create(
113
+ # path_str="a.txt", md5sum="h1", size=10, mtime=1, mimetype="t"
114
+ # )
115
+
116
+ # job = Job.formalize_intent(source, self.chat, False, 100)
117
+ # payload = Payload.create(job=job, sequence_index=0, md5sum="hp1", size=10)
118
+
119
+ # user = TelegramUser.create(id=123, first_name="Tester")
120
+
121
+ # # Mock de objeto Message de Pyrogram que cumpla con json.loads(str(msg))
122
+ # class MockTgMessage:
123
+ # def __init__(self):
124
+ # self.id = 999
125
+ # self.chat = type("obj", (object,), {"id": -100123456})
126
+
127
+ # def __str__(self):
128
+ # return json.dumps(
129
+ # {
130
+ # "message_id": self.id,
131
+ # "chat": {"id": self.chat.id, "type": "channel"}, # type: ignore
132
+ # }
133
+ # )
134
+
135
+ # remote = RemotePayload.register_upload(
136
+ # payload=payload, tg_message=MockTgMessage(), owner=user
137
+ # )
138
+
139
+ # self.assertEqual(remote.message_id, 999)
140
+ # self.assertEqual(remote.chat_id, -100123456)
141
+ # self.assertEqual(remote.owner.id, 123)