chore: secure ticketing configuration

This commit is contained in:
2026-07-24 09:20:16 +02:00
parent 29fd7d21c8
commit 7b04a1e803
9 changed files with 550 additions and 102 deletions
+2
View File
@@ -0,0 +1,2 @@
/ticketing.ini
/tickets_state.json
+121
View File
@@ -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 `<odoo.url>/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.
+95
View File
@@ -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`.
+44
View File
@@ -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==
-2
View File
@@ -1,2 +0,0 @@
[rocket]
url= https://chat.comitari.fr/hooks/64d4d02760b38508f62a5bcb/ncKSYRiLM9oNXagK5c7G3KWX2qEzET3kbFFXKnNAhtfZQEQ9
+157 -99
View File
@@ -1,125 +1,183 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from setuphelpers import * from setuphelpers import *
import xmlrpc.client
import pickle
import os
from configparser import ConfigParser
import requests
import json import json
import os
import shutil
from configparser import ConfigParser
# Configuration import requests
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}',
}
# Endpoint URL
endpoint = f'{url}/jsonrpc'
# Payload pour récupérer tous les tickets CONFIG_FILE = "ticketing.ini"
payload = { CONFIG_TEMPLATE_FILE = "ticketing.ini.example"
'jsonrpc': '2.0', STATE_FILE = "tickets_state.json"
'method': 'call', DEFAULT_TIMEOUT = 30
'params': {
'model': 'helpdesk.ticket',
'method': 'search_read', def get_config_path():
'args': [[], ['id', 'write_date']], return makepath(WAPT.private_dir, CONFIG_FILE)
},
'id': 1,
} def get_state_path():
return makepath(WAPT.private_dir, STATE_FILE)
def install(): 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(): def audit():
conf_wapt = load_config()
try: tickets = fetch_odoo_tickets(conf_wapt)
# Récupération de l'état actuel de tous les tickets previous_tickets_state = load_tickets_state(get_state_path())
response = requests.post(endpoint, json=payload, headers=headers) tickets_updated = get_updated_ticket_ids(tickets, previous_tickets_state)
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)
if tickets_updated: if tickets_updated:
message = f"Les tickets suivants ont été mis à jour depuis la dernière vérification : {tickets_updated}" message = "Les tickets suivants ont ete mis a jour depuis la derniere verification : %s" % tickets_updated
send_to_rocket(message)
else: else:
message = "Aucun ticket n'a été mis à jour depuis la dernière vérification." message = "Aucun ticket n'a ete mis a jour depuis la derniere verification."
send_to_rocket(message)
# Sauvegarde de l'état actuel pour la prochaine exécution send_to_rocket(message, conf_wapt=conf_wapt)
current_tickets_state = {ticket['id']: ticket['write_date'] for ticket in tickets} save_tickets_state(get_state_path(), build_tickets_state(tickets))
with open(state_file, 'wb') as f:
pickle.dump(current_tickets_state, f)
return "OK" return "OK"
def send_to_rocket(message_text, attachments=None): def load_config():
""" config_path = get_config_path()
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")
conf_wapt = ConfigParser() 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 = { message = {
'text': message_text "text": message_text,
} }
if attachments: if attachments:
message['attachments'] = 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}')
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.")
+121
View File
@@ -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()
+10
View File
@@ -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
-1
View File
@@ -1 +0,0 @@
}.