diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f955128 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/ticketing.ini +/tickets_state.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d21e0f --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# comi-odoo-ticketing + +Script WAPT de surveillance des tickets Odoo Helpdesk Comitari. + +Le projet interroge Odoo via JSON-RPC, compare la date de modification des tickets avec un etat local JSON, puis envoie un message Rocket.Chat via webhook quand des tickets ont change depuis la derniere verification. + +## Contenu du depot + +| Fichier | Role | +| --- | --- | +| `setup.py` | Script WAPT principal. Contient les fonctions `install()`, `audit()` et `send_to_rocket()`. | +| `WAPT/control` | Metadata minimale du paquet WAPT. | +| `ticketing.ini.example` | Modele de configuration avec placeholders, copie dans le repertoire prive WAPT sous le nom `ticketing.ini`. | +| `tests/test_ticketing.py` | Tests unitaires de la logique de comparaison et de persistance d'etat. | +| `.env` | Variables d'environnement locales pour pointer vers l'installation Python/WAPT Windows. | +| `TRAVAIL.md` | Journal de travail maintenu par Codex. | + +## Fonctionnement + +1. `install()` copie `ticketing.ini.example` vers `WAPT.private_dir/ticketing.ini` si le fichier cible n'existe pas encore. +2. Le fichier copie doit etre renseigne sur la machine cible avec l'URL Odoo, la cle API et le webhook Rocket.Chat. +3. `audit()` appelle l'endpoint JSON-RPC Odoo configure dans `ticketing.ini`. +4. La methode Odoo appelee est `helpdesk.ticket.search_read`, avec les champs `id` et `write_date`. +5. Le script charge l'ancien etat depuis `tickets_state.json`. +6. Chaque ticket nouveau ou dont `write_date` a change est ajoute a la liste des tickets mis a jour. +7. Un message est envoye dans Rocket.Chat via le webhook configure dans le fichier INI prive. +8. Le nouvel etat est sauvegarde dans `WAPT.private_dir/tickets_state.json`. + +## Prerequis + +- Environnement WAPT avec `setuphelpers`. +- Python compatible avec l'environnement WAPT cible. +- Acces reseau vers Odoo Comitari. +- Acces reseau vers Rocket.Chat Comitari. +- Dependances Python : + - `requests` + - modules standard : `json`, `os`, `shutil`, `configparser` + +## Configuration + +La configuration sensible est centralisee dans la copie privee `WAPT.private_dir/ticketing.ini`. Le depot ne versionne que `ticketing.ini.example`, avec des placeholders : + +```ini +[rocket] +webhook_url = CHANGE_ME_ROCKET_CHAT_WEBHOOK_URL + +[odoo] +url = https://CHANGE_ME_ODOO_HOST +api_key = CHANGE_ME_ODOO_API_KEY +model = helpdesk.ticket + +[http] +timeout = 30 +``` + +Au deploiement, `install()` copie ce modele vers `WAPT.private_dir/ticketing.ini` uniquement si le fichier n'existe pas deja. Il faut ensuite renseigner le fichier prive sur la machine cible. Les mises a jour du paquet n'ecrasent donc pas une configuration deja remplie. + +Si une valeur obligatoire est absente ou conserve un placeholder `CHANGE_ME...`, l'audit echoue explicitement avec un message indiquant la cle a renseigner. + +Important : ne pas renseigner de vrais secrets dans `ticketing.ini.example`. Le fichier local `ticketing.ini` est ignore par Git. + +## Execution + +Dans un paquet WAPT, l'execution attendue passe par les hooks WAPT : + +```python +install() +audit() +``` + +Hors WAPT, le script ne peut pas etre lance tel quel sans fournir les objets et chemins attendus par `setuphelpers`, notamment `WAPT.private_dir`. + +## Etat local + +`tickets_state.json` est stocke dans `WAPT.private_dir` et contient un dictionnaire JSON au format : + +```json +{ + "ticket_id": "write_date" +} +``` + +Le runtime lit et ecrit ce fichier dans `WAPT.private_dir`. Il n'est pas versionne dans le depot. + +## Protocole Odoo + +Le script utilise l'endpoint configure sous la forme `/jsonrpc` avec un payload JSON-RPC direct : + +```json +{ + "jsonrpc": "2.0", + "method": "call", + "params": { + "model": "helpdesk.ticket", + "method": "search_read", + "args": [[], ["id", "write_date"]] + }, + "id": 1 +} +``` + +Il faut donc que l'instance Odoo expose bien un endpoint compatible avec ce format. Si l'instance attend le protocole externe Odoo standard `execute_kw`, il faudra adapter `fetch_odoo_tickets()`. + +## Tests + +Les tests unitaires peuvent etre executes hors WAPT : + +```bash +python3 -m unittest discover -s tests +``` + +## Points d'attention connus + +- Les secrets doivent etre renseignes uniquement dans la copie privee `WAPT.private_dir/ticketing.ini`. +- Les secrets deja presents dans l'historique Git doivent etre regeneres cote Odoo et Rocket.Chat. + +## Prochaines ameliorations recommandees + +1. Confirmer en environnement cible que l'endpoint Odoo accepte bien le payload JSON-RPC direct documente ci-dessus. +2. Completer les metadonnees `WAPT/control` selon les conventions internes Comitari. +3. Ajouter un test d'integration manuel ou automatise sur une instance Odoo de recette. diff --git a/TRAVAIL.md b/TRAVAIL.md new file mode 100644 index 0000000..41b5ab1 --- /dev/null +++ b/TRAVAIL.md @@ -0,0 +1,95 @@ +# Journal de travail + +Ce fichier consigne les interventions Codex sur le projet afin de garder une trace exploitable des analyses, decisions et suites a traiter. + +## 2026-06-24 + +### Demande + +- Analyser le projet. +- Creer un README et le maintenir a jour. +- Alimenter un fichier Markdown dedie au travail effectue. + +### Analyse realisee + +- Le depot contient un script WAPT principal `setup.py`. +- Le script surveille les tickets Odoo Helpdesk via JSON-RPC. +- Il compare les champs `id` et `write_date` avec un etat local `tickets_state.pkl`. +- Il notifie Rocket.Chat via un webhook configure dans `rocket.ini`. +- Le fichier `tickets_state.pkl` initial a ete inspecte avec `pickletools` sans charger le pickle comme code executable ; il contient actuellement un dictionnaire vide. +- `.env` contient des chemins Windows vers l'environnement WAPT. + +### Fichiers ajoutes + +- `README.md` : documentation du projet, fonctionnement, prerequis, configuration, risques et ameliorations recommandees. +- `TRAVAIL.md` : journal de travail maintenu par Codex. + +### Points techniques identifies + +- Bug bloquant probable dans `setup.py` : `tickets_updated.auppend(ticket_id)` doit etre corrige en `tickets_updated.append(ticket_id)`. +- Secrets presents en clair : + - cle API Odoo dans `setup.py` ; + - webhook Rocket.Chat dans `rocket.ini`. +- `pickle.load()` est sensible si le fichier d'etat peut etre modifie par une source non fiable. +- `db` et `xmlrpc.client` semblent inutilises. +- Le chemin de `tickets_state.pkl` est relatif au repertoire courant. + +### Suites recommandees + +1. Corriger la faute `auppend`. +2. Sortir les secrets du depot. +3. Remplacer l'etat pickle par JSON. +4. Ajouter un test minimal sur la detection des tickets modifies. +5. Verifier la conformite du payload JSON-RPC avec l'API Odoo reellement exposee. + +### Modification configuration secrete + +- Retrait de la cle API Odoo codee en dur dans `setup.py`. +- Remplacement du webhook reel par des placeholders. +- Centralisation de la configuration sensible dans `ticketing.ini.example`, copiee en prive sous le nom `ticketing.ini` : + - `rocket.webhook_url` + - `odoo.url` + - `odoo.api_key` + - `odoo.model` +- `install()` copie le fichier INI modele vers `WAPT.private_dir/ticketing.ini` uniquement s'il n'existe pas deja, afin de ne pas ecraser une configuration renseignee sur la machine cible. +- `audit()` charge maintenant l'URL Odoo et la cle API depuis le fichier INI prive. +- `send_to_rocket()` charge maintenant le webhook depuis le fichier INI prive. +- Correction du bug `tickets_updated.auppend(ticket_id)` en `tickets_updated.append(ticket_id)`. +- Deplacement du fichier d'etat vers `WAPT.private_dir/tickets_state.pkl` pour eviter de dependre du repertoire courant. +- Ajout d'une validation explicite des placeholders `CHANGE_ME...` pour eviter une execution avec une configuration non renseignee. +- Ajout de `.gitignore` pour eviter de versionner un `ticketing.ini` rempli localement et un futur fichier d'etat runtime. + +## 2026-07-24 + +### Analyse des ameliorations possibles + +- Remplacer `pickle` par JSON pour le fichier d'etat runtime. +- Extraire la recuperation Odoo, la comparaison d'etat et l'envoi Rocket.Chat dans des fonctions testables. +- Ajouter des timeouts explicites aux appels HTTP Odoo et Rocket.Chat. +- Faire echouer l'audit si l'appel Odoo echoue, afin d'eviter d'ecraser l'etat avec une liste vide en cas d'indisponibilite. +- Utiliser `response.raise_for_status()` aussi pour l'appel Rocket.Chat. +- Ajouter des tests unitaires sur la detection des tickets nouveaux ou modifies. +- Retirer du depot le reliquat `tickets_state.pkl`, aujourd'hui remplace par l'etat stocke dans `WAPT.private_dir`. +- Ajouter un fichier de controle WAPT si le depot doit etre un paquet complet. +- Mettre en place une rotation des secrets deja presents dans l'historique Git. + +### Mise en oeuvre des ameliorations + +- Remplacement de l'etat runtime `tickets_state.pkl` par `tickets_state.json`. +- Suppression de l'utilisation de `pickle` dans le code. +- Ajout des fonctions testables : + - `build_odoo_payload()` + - `fetch_odoo_tickets()` + - `load_tickets_state()` + - `save_tickets_state()` + - `build_tickets_state()` + - `get_updated_ticket_ids()` +- Ajout d'un timeout HTTP configurable via `http.timeout`, avec valeur par defaut a 30 secondes. +- Durcissement de l'appel Odoo : les erreurs HTTP, les erreurs JSON-RPC et les reponses sans `result` font echouer l'audit au lieu de sauvegarder un etat vide. +- Durcissement de l'appel Rocket.Chat avec `raise_for_status()`. +- Ajout de `tests/test_ticketing.py` pour valider la comparaison des tickets et la lecture/ecriture JSON. +- Suppression du reliquat `tickets_state.pkl` du repertoire projet. +- Mise a jour de `README.md`, `ticketing.ini.example` et `.gitignore`. +- Ajout de tests avec mocks `requests` sur `fetch_odoo_tickets()` et `send_to_rocket()`. +- Ajout d'un fichier `WAPT/control` minimal. +- Documentation du payload JSON-RPC Odoo attendu dans `README.md`. diff --git a/WAPT/control b/WAPT/control new file mode 100644 index 0000000..9009038 --- /dev/null +++ b/WAPT/control @@ -0,0 +1,44 @@ +package : comi-odoo-ticketing +version : 0.1.0-2 +architecture : all +section : base +priority : optional +name : Comitari Odoo ticketing audit +categories : Utilities +maintainer : Comitari +description : Surveille les tickets Odoo Helpdesk et notifie Rocket.Chat via webhook. +depends : +conflicts : +maturity : PROD +locale : all +target_os : windows +min_wapt_version : +sources : +installed_size : +impacted_process : +description_fr : +description_pl : +description_de : +description_es : +description_pt : +description_it : +description_nl : +description_ru : +audit_schedule : +editor : +keywords : +licence : +homepage : +package_uuid : 0afd3de2-c939-918b-67b0-d79a8ccba84b +valid_from : +valid_until : +forced_install_on : +changelog : +min_os_version : +max_os_version : +icon_sha256sum : +signer : pcosson_key +signer_fingerprint: a25582410cf03bad179a60c189f459a0b03821c92c0cedf209e82448a66a9b4e +signature_date : 2026-07-24T07:19:02.000000 +signed_attributes : package,version,architecture,section,priority,name,categories,maintainer,description,depends,conflicts,maturity,locale,target_os,min_wapt_version,sources,installed_size,impacted_process,description_fr,description_pl,description_de,description_es,description_pt,description_it,description_nl,description_ru,audit_schedule,editor,keywords,licence,homepage,package_uuid,valid_from,valid_until,forced_install_on,changelog,min_os_version,max_os_version,icon_sha256sum,signer,signer_fingerprint,signature_date,signed_attributes +signature : dBwE3rudjuJOvYhduNbzizfvql8Fqo13fs6anaHSoRoBCnRteqzJy4JRmaA6OV+YJOtfC89E7OnNBKAOvO1YErsaanKzFG+wkZFBZEgGkBcJZsb0xec5iLHoslEt6ZJw9ldsSs0dzRH1tzIb7nthMi5AOkPPqICKf5mXkYiOtAcBG9HjFa8YeYtBwyFM5ME2XeKkqpsESh03cOseNLpQ9DB2vMe0qZHd14nPZe5TpOFen+F4xklHSfemiMU8AFPclewTuhkYC8mqps2+WkHmTWl35ilJGg8BC2nLxiydotmOwc+QqpAYw90N6r9lVnPUFa1Bh2QqnmJnk5bfePYKTQ== \ No newline at end of file diff --git a/rocket.ini b/rocket.ini deleted file mode 100644 index 0cd23b5..0000000 --- a/rocket.ini +++ /dev/null @@ -1,2 +0,0 @@ -[rocket] -url= https://chat.comitari.fr/hooks/64d4d02760b38508f62a5bcb/ncKSYRiLM9oNXagK5c7G3KWX2qEzET3kbFFXKnNAhtfZQEQ9 \ No newline at end of file diff --git a/setup.py b/setup.py index 1e496b8..0b87e30 100644 --- a/setup.py +++ b/setup.py @@ -1,125 +1,183 @@ # -*- coding: utf-8 -*- from setuphelpers import * -import xmlrpc.client -import pickle -import os -from configparser import ConfigParser -import requests import json +import os +import shutil +from configparser import ConfigParser -# Configuration -url = 'https://odoo.comitari.fr' -db = 'comitari' -state_file = 'tickets_state.pkl' -api_key = "182534f9e754ce1016e86b86b4b5b47199659a6f" -headers = { - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {api_key}', -} +import requests -# Endpoint URL -endpoint = f'{url}/jsonrpc' -# Payload pour récupérer tous les tickets -payload = { - 'jsonrpc': '2.0', - 'method': 'call', - 'params': { - 'model': 'helpdesk.ticket', - 'method': 'search_read', - 'args': [[], ['id', 'write_date']], - }, - 'id': 1, -} +CONFIG_FILE = "ticketing.ini" +CONFIG_TEMPLATE_FILE = "ticketing.ini.example" +STATE_FILE = "tickets_state.json" +DEFAULT_TIMEOUT = 30 + + +def get_config_path(): + return makepath(WAPT.private_dir, CONFIG_FILE) + + +def get_state_path(): + return makepath(WAPT.private_dir, STATE_FILE) + def install(): + config_path = get_config_path() - plugin_inifile = makepath(WAPT.private_dir, "rocket.ini") + if not isfile(config_path): + shutil.copyfile(CONFIG_TEMPLATE_FILE, config_path) - if not isfile(plugin_inifile): - filecopyto("rocket.ini", WAPT.private_dir) def audit(): - - try: - # Récupération de l'état actuel de tous les tickets - response = requests.post(endpoint, json=payload, headers=headers) - response.raise_for_status() # Vérifie si la requête a réussi - response_data = response.json() - - # Vérifie si 'result' est présent dans la réponse - if 'result' in response_data: - tickets = response_data['result'] - else: - print("Erreur: La clé 'result' n'est pas présente dans la réponse.") - print("Réponse complète:", response_data) - tickets = [] - except requests.exceptions.RequestException as e: - print(f"Erreur lors de la requête: {e}") - tickets = [] - - # Vérification si un état précédent existe - if os.path.exists(state_file): - with open(state_file, 'rb') as f: - previous_tickets_state = pickle.load(f) - else: - previous_tickets_state = {} - - - # Comparaison et affichage des résultats - tickets_updated = [] - for ticket in tickets: - ticket_id = ticket['id'] - ticket_write_date = ticket['write_date'] - - if ticket_id in previous_tickets_state: - if ticket_write_date != previous_tickets_state[ticket_id]: - tickets_updated.auppend(ticket_id) - else: - tickets_updated.append(ticket_id) + conf_wapt = load_config() + tickets = fetch_odoo_tickets(conf_wapt) + previous_tickets_state = load_tickets_state(get_state_path()) + tickets_updated = get_updated_ticket_ids(tickets, previous_tickets_state) if tickets_updated: - message = f"Les tickets suivants ont été mis à jour depuis la dernière vérification : {tickets_updated}" - send_to_rocket(message) - + message = "Les tickets suivants ont ete mis a jour depuis la derniere verification : %s" % tickets_updated else: - message = "Aucun ticket n'a été mis à jour depuis la dernière vérification." - send_to_rocket(message) - # Sauvegarde de l'état actuel pour la prochaine exécution - current_tickets_state = {ticket['id']: ticket['write_date'] for ticket in tickets} - with open(state_file, 'wb') as f: - pickle.dump(current_tickets_state, f) + message = "Aucun ticket n'a ete mis a jour depuis la derniere verification." + + send_to_rocket(message, conf_wapt=conf_wapt) + save_tickets_state(get_state_path(), build_tickets_state(tickets)) return "OK" -def send_to_rocket(message_text, attachments=None): - """ - Envoie un message à Rocket.Chat via un webhook. - - :param message_text: Texte du message à envoyer - :param attachments: Liste de pièces jointes (facultatif) - """ - smtp_inifile = makepath(WAPT.private_dir, "rocket.ini") +def load_config(): + config_path = get_config_path() conf_wapt = ConfigParser() - conf_wapt.read(smtp_inifile) + conf_wapt.read(config_path) - webhook_url = conf_wapt.get("rocket", "url") + required_options = [ + ("odoo", "url"), + ("odoo", "api_key"), + ("rocket", "webhook_url"), + ] + missing_options = [ + "%s.%s" % (section, option) + for section, option in required_options + if not conf_wapt.has_option(section, option) + ] - # Construire le message + if missing_options: + raise Exception( + "Configuration incomplete dans %s : %s" + % (config_path, ", ".join(missing_options)) + ) + + placeholder_options = [ + "%s.%s" % (section, option) + for section, option in required_options + if "CHANGE_ME" in conf_wapt.get(section, option).strip() + ] + if placeholder_options: + raise Exception( + "Configuration a renseigner dans %s : %s" + % (config_path, ", ".join(placeholder_options)) + ) + + return conf_wapt + + +def get_http_timeout(conf_wapt): + return conf_wapt.getint("http", "timeout", fallback=DEFAULT_TIMEOUT) + + +def build_odoo_payload(conf_wapt): + return { + "jsonrpc": "2.0", + "method": "call", + "params": { + "model": conf_wapt.get("odoo", "model", fallback="helpdesk.ticket"), + "method": "search_read", + "args": [[], ["id", "write_date"]], + }, + "id": 1, + } + + +def fetch_odoo_tickets(conf_wapt): + odoo_url = conf_wapt.get("odoo", "url").rstrip("/") + endpoint = "%s/jsonrpc" % odoo_url + headers = { + "Content-Type": "application/json", + "Authorization": "Bearer %s" % conf_wapt.get("odoo", "api_key"), + } + + response = requests.post( + endpoint, + json=build_odoo_payload(conf_wapt), + headers=headers, + timeout=get_http_timeout(conf_wapt), + ) + response.raise_for_status() + response_data = response.json() + + if "error" in response_data: + raise Exception("Erreur Odoo JSON-RPC : %s" % response_data["error"]) + if "result" not in response_data: + raise Exception("Reponse Odoo invalide : cle 'result' absente") + + return response_data["result"] + + +def load_tickets_state(state_path): + if not os.path.exists(state_path): + return {} + + with open(state_path, "r") as state_file: + return json.load(state_file) + + +def save_tickets_state(state_path, tickets_state): + with open(state_path, "w") as state_file: + json.dump(tickets_state, state_file, indent=2, sort_keys=True) + + +def build_tickets_state(tickets): + return { + str(ticket["id"]): ticket["write_date"] + for ticket in tickets + } + + +def get_updated_ticket_ids(tickets, previous_tickets_state): + tickets_updated = [] + + for ticket in tickets: + ticket_id = str(ticket["id"]) + ticket_write_date = ticket["write_date"] + + if previous_tickets_state.get(ticket_id) != ticket_write_date: + tickets_updated.append(ticket["id"]) + + return tickets_updated + + +def send_to_rocket(message_text, attachments=None, conf_wapt=None): + """ + Envoie un message a Rocket.Chat via un webhook. + + :param message_text: Texte du message a envoyer + :param attachments: Liste de pieces jointes (facultatif) + :param conf_wapt: Configuration deja chargee (facultatif) + """ + if conf_wapt is None: + conf_wapt = load_config() message = { - 'text': message_text + "text": message_text, } if attachments: - message['attachments'] = attachments - - # Envoyer la requête POST - response = requests.post(webhook_url, data=json.dumps(message), headers={'Content-Type': 'application/json'}) - - # Vérifier la réponse - if response.status_code == 200: - print('Message envoyé avec succès.') - else: - print(f'Échec de l\'envoi du message. Statut de la réponse : {response.status_code}') - print(f'Erreur : {response.text}') + message["attachments"] = attachments + response = requests.post( + conf_wapt.get("rocket", "webhook_url"), + json=message, + headers={"Content-Type": "application/json"}, + timeout=get_http_timeout(conf_wapt), + ) + response.raise_for_status() + print("Message envoye avec succes.") diff --git a/tests/test_ticketing.py b/tests/test_ticketing.py new file mode 100644 index 0000000..350536d --- /dev/null +++ b/tests/test_ticketing.py @@ -0,0 +1,121 @@ +import importlib.util +import os +import sys +import tempfile +import types +import unittest +from configparser import ConfigParser +from unittest.mock import Mock, patch + + +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SETUP_PATH = os.path.join(PROJECT_ROOT, "setup.py") + + +def load_setup_module(): + setuphelpers = types.ModuleType("setuphelpers") + setuphelpers.makepath = lambda *parts: os.path.join(*parts) + setuphelpers.isfile = os.path.isfile + setuphelpers.WAPT = types.SimpleNamespace(private_dir=tempfile.gettempdir()) + sys.modules["setuphelpers"] = setuphelpers + + spec = importlib.util.spec_from_file_location("ticketing_setup", SETUP_PATH) + module = importlib.util.module_from_spec(spec) + sys.modules["ticketing_setup"] = module + spec.loader.exec_module(module) + return module + + +ticketing = load_setup_module() + + +def make_config(): + config = ConfigParser() + config.add_section("odoo") + config.set("odoo", "url", "https://odoo.example.test") + config.set("odoo", "api_key", "test-api-key") + config.set("odoo", "model", "helpdesk.ticket") + config.add_section("rocket") + config.set("rocket", "webhook_url", "https://rocket.example.test/webhook-test") + config.add_section("http") + config.set("http", "timeout", "5") + return config + + +class TicketingStateTest(unittest.TestCase): + def test_build_tickets_state_uses_string_ids_for_json_keys(self): + state = ticketing.build_tickets_state([ + {"id": 12, "write_date": "2026-07-24 10:00:00"}, + {"id": 34, "write_date": "2026-07-24 11:00:00"}, + ]) + + self.assertEqual(state, { + "12": "2026-07-24 10:00:00", + "34": "2026-07-24 11:00:00", + }) + + def test_get_updated_ticket_ids_detects_new_and_changed_tickets(self): + previous_state = { + "1": "2026-07-24 09:00:00", + "2": "2026-07-24 09:30:00", + } + tickets = [ + {"id": 1, "write_date": "2026-07-24 09:00:00"}, + {"id": 2, "write_date": "2026-07-24 10:00:00"}, + {"id": 3, "write_date": "2026-07-24 10:30:00"}, + ] + + self.assertEqual(ticketing.get_updated_ticket_ids(tickets, previous_state), [2, 3]) + + def test_load_tickets_state_returns_empty_dict_when_file_is_missing(self): + missing_path = os.path.join(tempfile.gettempdir(), "missing-ticket-state.json") + + self.assertEqual(ticketing.load_tickets_state(missing_path), {}) + + def test_save_and_load_tickets_state_roundtrip(self): + with tempfile.TemporaryDirectory() as tmpdir: + state_path = os.path.join(tmpdir, "tickets_state.json") + expected_state = {"42": "2026-07-24 12:00:00"} + + ticketing.save_tickets_state(state_path, expected_state) + + self.assertEqual(ticketing.load_tickets_state(state_path), expected_state) + + @patch("ticketing_setup.requests.post") + def test_fetch_odoo_tickets_uses_timeout_and_returns_result(self, post): + response = Mock() + response.json.return_value = { + "result": [{"id": 42, "write_date": "2026-07-24 12:00:00"}], + } + post.return_value = response + + tickets = ticketing.fetch_odoo_tickets(make_config()) + + self.assertEqual(tickets, [{"id": 42, "write_date": "2026-07-24 12:00:00"}]) + response.raise_for_status.assert_called_once_with() + post.assert_called_once() + self.assertEqual(post.call_args.kwargs["timeout"], 5) + + @patch("ticketing_setup.requests.post") + def test_fetch_odoo_tickets_rejects_jsonrpc_error(self, post): + response = Mock() + response.json.return_value = {"error": {"message": "Access denied"}} + post.return_value = response + + with self.assertRaisesRegex(Exception, "Erreur Odoo JSON-RPC"): + ticketing.fetch_odoo_tickets(make_config()) + + @patch("ticketing_setup.requests.post") + def test_send_to_rocket_raises_for_http_errors(self, post): + response = Mock() + post.return_value = response + + ticketing.send_to_rocket("message de test", conf_wapt=make_config()) + + response.raise_for_status.assert_called_once_with() + self.assertEqual(post.call_args.kwargs["timeout"], 5) + self.assertEqual(post.call_args.kwargs["json"], {"text": "message de test"}) + + +if __name__ == "__main__": + unittest.main() diff --git a/ticketing.ini.example b/ticketing.ini.example new file mode 100644 index 0000000..e057865 --- /dev/null +++ b/ticketing.ini.example @@ -0,0 +1,10 @@ +[rocket] +webhook_url = CHANGE_ME_ROCKET_CHAT_WEBHOOK_URL + +[odoo] +url = https://CHANGE_ME_ODOO_HOST +api_key = CHANGE_ME_ODOO_API_KEY +model = helpdesk.ticket + +[http] +timeout = 30 diff --git a/tickets_state.pkl b/tickets_state.pkl deleted file mode 100644 index e2ecf72..0000000 --- a/tickets_state.pkl +++ /dev/null @@ -1 +0,0 @@ -€}”. \ No newline at end of file