weni-cli 3.7.2__tar.gz → 3.8.0__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.
- {weni_cli-3.7.2 → weni_cli-3.8.0}/PKG-INFO +1 -1
- {weni_cli-3.7.2 → weni_cli-3.8.0}/pyproject.toml +1 -1
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/cli.py +21 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/cli_client.py +18 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/tests/test_cli_client.py +87 -0
- weni_cli-3.8.0/weni_cli/commands/ticketer_create.py +63 -0
- weni_cli-3.8.0/weni_cli/validators/tests/test_ticketer_definition.py +206 -0
- weni_cli-3.8.0/weni_cli/validators/ticketer_definition.py +177 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/README.md +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/__init__.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/auth.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/__init__.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/common.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/response_handlers/__init__.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/response_handlers/handlers.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/tests/__init__.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/tests/test_weni_client.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/weni_client.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/channel_create.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/eval_init.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/eval_run.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/init.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/login.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/logs.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/project_current.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/project_list.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/project_push.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/project_use.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/run.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/formatter/__init__.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/formatter/formatter.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/formatter/tests/test_formatter.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/handler.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/packager/__init__.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/packager/loader.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/packager/packager.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/packager/tests/__init__.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/packager/tests/test_loader.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/packager/tests/test_packager.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/spinner/__init__.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/spinner/spinners.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/store.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/utils.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/__init__.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/agent_definition.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/channel_definition.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/source.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/tests/__init__.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/tests/conftest.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/tests/test_active_test_definition.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/tests/test_channel_definition.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/tests/test_definition.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/tests/test_source.py +0 -0
- {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/wsgi.py +0 -0
|
@@ -160,6 +160,27 @@ def create_channel(channel_definition):
|
|
|
160
160
|
click.echo(f"Error: {e}")
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
@cli.group()
|
|
164
|
+
def ticketer():
|
|
165
|
+
"""Ticketer management commands"""
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@ticketer.command("create")
|
|
170
|
+
@click.argument("ticketer_definition", required=True, type=click.Path(exists=True, dir_okay=False))
|
|
171
|
+
def create_ticketer(ticketer_definition):
|
|
172
|
+
"""Create a new ticketer from a definition file
|
|
173
|
+
|
|
174
|
+
TICKETER_DEFINITION: The path to the YAML ticketer definition file
|
|
175
|
+
"""
|
|
176
|
+
from weni_cli.commands.ticketer_create import TicketerCreateHandler
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
TicketerCreateHandler().execute(ticketer_definition=ticketer_definition)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
click.echo(f"Error: {e}")
|
|
182
|
+
|
|
183
|
+
|
|
163
184
|
@cli.group()
|
|
164
185
|
def eval():
|
|
165
186
|
"""Agent evaluation commands"""
|
|
@@ -382,3 +382,21 @@ class CLIClient:
|
|
|
382
382
|
return response.json()
|
|
383
383
|
except RequestError as e:
|
|
384
384
|
raise RequestError(f"Failed to create channel: {e.message}")
|
|
385
|
+
|
|
386
|
+
def create_ticketer(self, project_uuid: str, ticketer_definition: Dict) -> Dict[str, Any]:
|
|
387
|
+
"""Create a ticketer."""
|
|
388
|
+
if "ticketers" not in ticketer_definition or not ticketer_definition["ticketers"]:
|
|
389
|
+
raise ValueError("No ticketers found in definition")
|
|
390
|
+
|
|
391
|
+
ticketer_data = ticketer_definition["ticketers"][0]
|
|
392
|
+
|
|
393
|
+
payload = {
|
|
394
|
+
"project_uuid": project_uuid,
|
|
395
|
+
"ticketer_definition": ticketer_data,
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
response = self._make_request(method="POST", endpoint="api/v1/ticketers", json_data=payload)
|
|
400
|
+
return response.json()
|
|
401
|
+
except RequestError as e:
|
|
402
|
+
raise RequestError(f"Failed to create ticketer: {e.message}")
|
|
@@ -1169,3 +1169,90 @@ def test_create_channel_with_whatsapp_channel_type(client, mocker):
|
|
|
1169
1169
|
"channel_definition": channel_definition["channels"][0]
|
|
1170
1170
|
}
|
|
1171
1171
|
)
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def test_create_ticketer_success(client, mocker):
|
|
1175
|
+
"""Test successful ticketer creation."""
|
|
1176
|
+
mock_response = mocker.MagicMock()
|
|
1177
|
+
mock_response.status_code = 200
|
|
1178
|
+
mock_response.json.return_value = {"uuid": "ticketer-uuid", "name": "org support"}
|
|
1179
|
+
mocker.patch.object(client, "_make_request", return_value=mock_response)
|
|
1180
|
+
|
|
1181
|
+
project_uuid = "test-project-uuid"
|
|
1182
|
+
ticketer_definition = {
|
|
1183
|
+
"ticketers": [
|
|
1184
|
+
{
|
|
1185
|
+
"name": "org support",
|
|
1186
|
+
"ticketer_type": "generic",
|
|
1187
|
+
"config": {
|
|
1188
|
+
"base_url": "https://example.com",
|
|
1189
|
+
"api_token": "test-api-token",
|
|
1190
|
+
"webhook_secret": "test-webhook-secret",
|
|
1191
|
+
},
|
|
1192
|
+
}
|
|
1193
|
+
]
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
client.create_ticketer(project_uuid, ticketer_definition)
|
|
1197
|
+
|
|
1198
|
+
client._make_request.assert_called_once_with(
|
|
1199
|
+
method="POST",
|
|
1200
|
+
endpoint="api/v1/ticketers",
|
|
1201
|
+
json_data={
|
|
1202
|
+
"project_uuid": project_uuid,
|
|
1203
|
+
"ticketer_definition": ticketer_definition["ticketers"][0],
|
|
1204
|
+
},
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
def test_create_ticketer_no_ticketers_in_definition(client):
|
|
1209
|
+
"""Test ticketer creation with no ticketers in definition."""
|
|
1210
|
+
project_uuid = "test-project-uuid"
|
|
1211
|
+
ticketer_definition = {}
|
|
1212
|
+
|
|
1213
|
+
with pytest.raises(ValueError) as exc_info:
|
|
1214
|
+
client.create_ticketer(project_uuid, ticketer_definition)
|
|
1215
|
+
|
|
1216
|
+
assert "No ticketers found in definition" in str(exc_info.value)
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def test_create_ticketer_empty_ticketers_array(client):
|
|
1220
|
+
"""Test ticketer creation with empty ticketers array."""
|
|
1221
|
+
project_uuid = "test-project-uuid"
|
|
1222
|
+
ticketer_definition = {"ticketers": []}
|
|
1223
|
+
|
|
1224
|
+
with pytest.raises(ValueError) as exc_info:
|
|
1225
|
+
client.create_ticketer(project_uuid, ticketer_definition)
|
|
1226
|
+
|
|
1227
|
+
assert "No ticketers found in definition" in str(exc_info.value)
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def test_create_ticketer_request_error(client, mocker):
|
|
1231
|
+
"""Test ticketer creation with request error."""
|
|
1232
|
+
error_message = "Failed to create ticketer in Flows"
|
|
1233
|
+
mocker.patch.object(
|
|
1234
|
+
client,
|
|
1235
|
+
"_make_request",
|
|
1236
|
+
side_effect=RequestError(message=error_message, status_code=400),
|
|
1237
|
+
)
|
|
1238
|
+
|
|
1239
|
+
project_uuid = "test-project-uuid"
|
|
1240
|
+
ticketer_definition = {
|
|
1241
|
+
"ticketers": [
|
|
1242
|
+
{
|
|
1243
|
+
"name": "org support",
|
|
1244
|
+
"ticketer_type": "generic",
|
|
1245
|
+
"config": {
|
|
1246
|
+
"base_url": "https://example.com",
|
|
1247
|
+
"api_token": "test-api-token",
|
|
1248
|
+
"webhook_secret": "test-webhook-secret",
|
|
1249
|
+
},
|
|
1250
|
+
}
|
|
1251
|
+
]
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
with pytest.raises(RequestError) as exc_info:
|
|
1255
|
+
client.create_ticketer(project_uuid, ticketer_definition)
|
|
1256
|
+
|
|
1257
|
+
assert "Failed to create ticketer" in str(exc_info.value)
|
|
1258
|
+
assert error_message in str(exc_info.value)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from weni_cli.clients.cli_client import CLIClient
|
|
2
|
+
from weni_cli.formatter.formatter import Formatter
|
|
3
|
+
from weni_cli.handler import Handler
|
|
4
|
+
from weni_cli.store import STORE_PROJECT_UUID_KEY, Store
|
|
5
|
+
from weni_cli.validators.ticketer_definition import (
|
|
6
|
+
load_ticketer_definition,
|
|
7
|
+
validate_ticketer_definition_schema,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TicketerCreateHandler(Handler):
|
|
12
|
+
def execute(self, **kwargs):
|
|
13
|
+
ticketer_definition_path = kwargs.get("ticketer_definition")
|
|
14
|
+
|
|
15
|
+
formatter = Formatter()
|
|
16
|
+
|
|
17
|
+
if not ticketer_definition_path:
|
|
18
|
+
formatter.print_error_panel("Ticketer definition path is required")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
store = Store()
|
|
22
|
+
project_uuid = store.get(STORE_PROJECT_UUID_KEY)
|
|
23
|
+
|
|
24
|
+
if not project_uuid:
|
|
25
|
+
formatter.print_error_panel("No project selected, please select a project first")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
ticketer_data, error = load_ticketer_definition(ticketer_definition_path)
|
|
29
|
+
if error:
|
|
30
|
+
formatter.print_error_panel(error)
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
schema_error = validate_ticketer_definition_schema(ticketer_data)
|
|
34
|
+
if schema_error:
|
|
35
|
+
formatter.print_error_panel(schema_error)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
self._ensure_project_uuid(ticketer_data, project_uuid)
|
|
39
|
+
|
|
40
|
+
client = CLIClient()
|
|
41
|
+
response = client.create_ticketer(project_uuid, ticketer_data)
|
|
42
|
+
|
|
43
|
+
ticketer_name = response.get("name") if isinstance(response, dict) else None
|
|
44
|
+
ticketer_uuid = response.get("uuid") if isinstance(response, dict) else None
|
|
45
|
+
|
|
46
|
+
details = []
|
|
47
|
+
if ticketer_name:
|
|
48
|
+
details.append(f"Name: {ticketer_name}")
|
|
49
|
+
if ticketer_uuid:
|
|
50
|
+
details.append(f"UUID: {ticketer_uuid}")
|
|
51
|
+
|
|
52
|
+
if details:
|
|
53
|
+
formatter.print_success_panel("Ticketer created successfully\n" + "\n".join(details))
|
|
54
|
+
else:
|
|
55
|
+
formatter.print_success_panel("Ticketer created successfully")
|
|
56
|
+
|
|
57
|
+
def _ensure_project_uuid(self, ticketer_data, project_uuid):
|
|
58
|
+
if not ticketer_data.get("ticketers"):
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
config = ticketer_data["ticketers"][0].setdefault("config", {})
|
|
62
|
+
if not config.get("project_uuid", "").strip():
|
|
63
|
+
config["project_uuid"] = project_uuid
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from click.testing import CliRunner
|
|
3
|
+
|
|
4
|
+
from weni_cli.validators.ticketer_definition import (
|
|
5
|
+
AVAILABLE_TICKETER_TYPES,
|
|
6
|
+
MAX_TICKETER_NAME_LENGTH,
|
|
7
|
+
load_ticketer_definition,
|
|
8
|
+
validate_ticketer_definition_schema,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def sample_definition_file():
|
|
14
|
+
"""Create a sample ticketer definition file for testing."""
|
|
15
|
+
runner = CliRunner()
|
|
16
|
+
with runner.isolated_filesystem():
|
|
17
|
+
with open("valid_ticketer_definition.yaml", "w") as f:
|
|
18
|
+
f.write(
|
|
19
|
+
"""
|
|
20
|
+
ticketers:
|
|
21
|
+
- name: org support
|
|
22
|
+
ticketer_type: generic
|
|
23
|
+
config:
|
|
24
|
+
base_url: https://example.com
|
|
25
|
+
api_token: test-api-token
|
|
26
|
+
webhook_secret: test-webhook-secret
|
|
27
|
+
project_name: org support
|
|
28
|
+
"""
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
with open("empty_ticketer_definition.yaml", "w") as f:
|
|
32
|
+
f.write("")
|
|
33
|
+
|
|
34
|
+
yield {
|
|
35
|
+
"valid_path": "valid_ticketer_definition.yaml",
|
|
36
|
+
"empty_path": "empty_ticketer_definition.yaml",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def valid_ticketer_definition():
|
|
42
|
+
"""Return a valid ticketer definition dictionary."""
|
|
43
|
+
return {
|
|
44
|
+
"ticketers": [
|
|
45
|
+
{
|
|
46
|
+
"name": "org support",
|
|
47
|
+
"ticketer_type": "generic",
|
|
48
|
+
"config": {
|
|
49
|
+
"base_url": "https://example.com",
|
|
50
|
+
"api_token": "test-api-token",
|
|
51
|
+
"webhook_secret": "test-webhook-secret",
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestValidateTicketerDefinitionSchema:
|
|
59
|
+
"""Tests for validate_ticketer_definition_schema function."""
|
|
60
|
+
|
|
61
|
+
def test_valid_ticketer_definition(self, valid_ticketer_definition):
|
|
62
|
+
error = validate_ticketer_definition_schema(valid_ticketer_definition)
|
|
63
|
+
assert error is None
|
|
64
|
+
|
|
65
|
+
def test_valid_ticketer_definition_with_skip_hmac(self, valid_ticketer_definition):
|
|
66
|
+
data = valid_ticketer_definition.copy()
|
|
67
|
+
data["ticketers"] = [dict(data["ticketers"][0])]
|
|
68
|
+
data["ticketers"][0]["config"] = dict(data["ticketers"][0]["config"])
|
|
69
|
+
del data["ticketers"][0]["config"]["webhook_secret"]
|
|
70
|
+
data["ticketers"][0]["config"]["skip_webhook_hmac"] = "true"
|
|
71
|
+
error = validate_ticketer_definition_schema(data)
|
|
72
|
+
assert error is None
|
|
73
|
+
|
|
74
|
+
def test_missing_ticketers_key(self):
|
|
75
|
+
error = validate_ticketer_definition_schema({})
|
|
76
|
+
assert error == "Missing required root key 'ticketers' in the ticketer definition file"
|
|
77
|
+
|
|
78
|
+
def test_ticketers_not_array(self):
|
|
79
|
+
error = validate_ticketer_definition_schema({"ticketers": {}})
|
|
80
|
+
assert error == "'ticketers' must be an array in the ticketer definition file"
|
|
81
|
+
|
|
82
|
+
def test_empty_ticketers_array(self):
|
|
83
|
+
error = validate_ticketer_definition_schema({"ticketers": []})
|
|
84
|
+
assert error == "No ticketers defined in the ticketer definition file"
|
|
85
|
+
|
|
86
|
+
def test_ticketer_not_object(self):
|
|
87
|
+
error = validate_ticketer_definition_schema({"ticketers": ["not an object"]})
|
|
88
|
+
assert error == "Ticketer at index 0 must be an object in the ticketer definition file"
|
|
89
|
+
|
|
90
|
+
def test_missing_name(self, valid_ticketer_definition):
|
|
91
|
+
data = valid_ticketer_definition.copy()
|
|
92
|
+
data["ticketers"] = [dict(data["ticketers"][0])]
|
|
93
|
+
del data["ticketers"][0]["name"]
|
|
94
|
+
error = validate_ticketer_definition_schema(data)
|
|
95
|
+
assert error == "Ticketer at index 0 is missing required field 'name' in the ticketer definition file"
|
|
96
|
+
|
|
97
|
+
def test_name_too_long(self, valid_ticketer_definition):
|
|
98
|
+
data = valid_ticketer_definition.copy()
|
|
99
|
+
data["ticketers"] = [dict(data["ticketers"][0])]
|
|
100
|
+
data["ticketers"][0]["name"] = "x" * (MAX_TICKETER_NAME_LENGTH + 1)
|
|
101
|
+
error = validate_ticketer_definition_schema(data)
|
|
102
|
+
assert (
|
|
103
|
+
error
|
|
104
|
+
== f"Ticketer at index 0: 'name' must be less than {MAX_TICKETER_NAME_LENGTH} characters in the ticketer definition file"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def test_missing_ticketer_type(self, valid_ticketer_definition):
|
|
108
|
+
data = valid_ticketer_definition.copy()
|
|
109
|
+
data["ticketers"] = [dict(data["ticketers"][0])]
|
|
110
|
+
del data["ticketers"][0]["ticketer_type"]
|
|
111
|
+
error = validate_ticketer_definition_schema(data)
|
|
112
|
+
assert (
|
|
113
|
+
error
|
|
114
|
+
== "Ticketer at index 0 is missing required field 'ticketer_type' in the ticketer definition file"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def test_invalid_ticketer_type(self, valid_ticketer_definition):
|
|
118
|
+
data = valid_ticketer_definition.copy()
|
|
119
|
+
data["ticketers"] = [dict(data["ticketers"][0])]
|
|
120
|
+
data["ticketers"][0]["ticketer_type"] = "invalid"
|
|
121
|
+
error = validate_ticketer_definition_schema(data)
|
|
122
|
+
assert (
|
|
123
|
+
error
|
|
124
|
+
== f"Ticketer at index 0: 'ticketer_type' must be one of: {', '.join(AVAILABLE_TICKETER_TYPES)} in the ticketer definition file"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def test_missing_config(self, valid_ticketer_definition):
|
|
128
|
+
data = valid_ticketer_definition.copy()
|
|
129
|
+
data["ticketers"] = [dict(data["ticketers"][0])]
|
|
130
|
+
del data["ticketers"][0]["config"]
|
|
131
|
+
error = validate_ticketer_definition_schema(data)
|
|
132
|
+
assert error == "Ticketer at index 0 is missing required field 'config' in the ticketer definition file"
|
|
133
|
+
|
|
134
|
+
def test_missing_base_url(self, valid_ticketer_definition):
|
|
135
|
+
data = valid_ticketer_definition.copy()
|
|
136
|
+
data["ticketers"] = [dict(data["ticketers"][0])]
|
|
137
|
+
data["ticketers"][0]["config"] = dict(data["ticketers"][0]["config"])
|
|
138
|
+
del data["ticketers"][0]["config"]["base_url"]
|
|
139
|
+
error = validate_ticketer_definition_schema(data)
|
|
140
|
+
assert (
|
|
141
|
+
error
|
|
142
|
+
== "Ticketer at index 0: 'config' is missing required field 'base_url' in the ticketer definition file"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def test_invalid_base_url(self, valid_ticketer_definition):
|
|
146
|
+
data = valid_ticketer_definition.copy()
|
|
147
|
+
data["ticketers"] = [dict(data["ticketers"][0])]
|
|
148
|
+
data["ticketers"][0]["config"] = dict(data["ticketers"][0]["config"])
|
|
149
|
+
data["ticketers"][0]["config"]["base_url"] = "not-a-valid-url"
|
|
150
|
+
error = validate_ticketer_definition_schema(data)
|
|
151
|
+
assert (
|
|
152
|
+
error
|
|
153
|
+
== "Ticketer at index 0: 'config.base_url' must be a valid URL starting with http:// or https:// in the ticketer definition file"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def test_missing_webhook_secret_without_skip_hmac(self, valid_ticketer_definition):
|
|
157
|
+
data = valid_ticketer_definition.copy()
|
|
158
|
+
data["ticketers"] = [dict(data["ticketers"][0])]
|
|
159
|
+
data["ticketers"][0]["config"] = dict(data["ticketers"][0]["config"])
|
|
160
|
+
del data["ticketers"][0]["config"]["webhook_secret"]
|
|
161
|
+
error = validate_ticketer_definition_schema(data)
|
|
162
|
+
assert (
|
|
163
|
+
error
|
|
164
|
+
== "Ticketer at index 0: 'config.webhook_secret' is required unless "
|
|
165
|
+
"'config.skip_webhook_hmac' is set to true, 1 or yes in the ticketer definition file"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def test_config_value_not_string(self, valid_ticketer_definition):
|
|
169
|
+
data = valid_ticketer_definition.copy()
|
|
170
|
+
data["ticketers"] = [dict(data["ticketers"][0])]
|
|
171
|
+
data["ticketers"][0]["config"] = dict(data["ticketers"][0]["config"])
|
|
172
|
+
data["ticketers"][0]["config"]["api_token"] = 123
|
|
173
|
+
error = validate_ticketer_definition_schema(data)
|
|
174
|
+
assert (
|
|
175
|
+
error == "Ticketer at index 0: 'config.api_token' must be a string in the ticketer definition file"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def test_unknown_config_field(self, valid_ticketer_definition):
|
|
179
|
+
data = valid_ticketer_definition.copy()
|
|
180
|
+
data["ticketers"] = [dict(data["ticketers"][0])]
|
|
181
|
+
data["ticketers"][0]["config"] = dict(data["ticketers"][0]["config"])
|
|
182
|
+
data["ticketers"][0]["config"]["unknown_field"] = "value"
|
|
183
|
+
error = validate_ticketer_definition_schema(data)
|
|
184
|
+
assert (
|
|
185
|
+
error
|
|
186
|
+
== "Ticketer at index 0: 'config.unknown_field' is not a recognized field in the ticketer definition file"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class TestLoadTicketerDefinition:
|
|
191
|
+
"""Tests for load_ticketer_definition function."""
|
|
192
|
+
|
|
193
|
+
def test_load_valid_ticketer_definition(self, sample_definition_file):
|
|
194
|
+
data, error = load_ticketer_definition(sample_definition_file["valid_path"])
|
|
195
|
+
assert error is None
|
|
196
|
+
assert "ticketers" in data
|
|
197
|
+
|
|
198
|
+
def test_load_empty_ticketer_definition(self, sample_definition_file):
|
|
199
|
+
data, error = load_ticketer_definition(sample_definition_file["empty_path"])
|
|
200
|
+
assert data is None
|
|
201
|
+
assert str(error) == "Empty definition file"
|
|
202
|
+
|
|
203
|
+
def test_load_nonexistent_file(self):
|
|
204
|
+
data, error = load_ticketer_definition("nonexistent.yaml")
|
|
205
|
+
assert data is None
|
|
206
|
+
assert error is not None
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from weni_cli.validators.channel_definition import load_yaml_file
|
|
5
|
+
|
|
6
|
+
# Constants for ticketer validation
|
|
7
|
+
MAX_TICKETER_NAME_LENGTH = 100
|
|
8
|
+
AVAILABLE_TICKETER_TYPES = ["generic"]
|
|
9
|
+
|
|
10
|
+
REQUIRED_CONFIG_FIELDS = ["base_url", "api_token"]
|
|
11
|
+
|
|
12
|
+
OPTIONAL_CONFIG_FIELDS = [
|
|
13
|
+
"skip_webhook_hmac",
|
|
14
|
+
"project_uuid",
|
|
15
|
+
"project_name",
|
|
16
|
+
"route_open",
|
|
17
|
+
"route_forward",
|
|
18
|
+
"route_close",
|
|
19
|
+
"route_reopen",
|
|
20
|
+
"route_history",
|
|
21
|
+
"webhook_secret",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
URL_PATTERN = re.compile(r"^https?://")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def skip_webhook_hmac_enabled(value: str) -> bool:
|
|
28
|
+
"""Return True when skip_webhook_hmac disables HMAC verification."""
|
|
29
|
+
normalized = value.strip().lower()
|
|
30
|
+
return normalized in ("true", "1", "yes")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_ticketer_definition_schema(data):
|
|
34
|
+
"""
|
|
35
|
+
Validates that the ticketer definition YAML contains all required fields and has a valid structure.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
data (dict): Parsed YAML data
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
str | None: Error message if validation fails, None otherwise
|
|
42
|
+
"""
|
|
43
|
+
if data.get("ticketers") is None:
|
|
44
|
+
return "Missing required root key 'ticketers' in the ticketer definition file"
|
|
45
|
+
|
|
46
|
+
if not isinstance(data["ticketers"], list):
|
|
47
|
+
return "'ticketers' must be an array in the ticketer definition file"
|
|
48
|
+
|
|
49
|
+
if not data["ticketers"]:
|
|
50
|
+
return "No ticketers defined in the ticketer definition file"
|
|
51
|
+
|
|
52
|
+
for ticketer_idx, ticketer_data in enumerate(data["ticketers"]):
|
|
53
|
+
if not isinstance(ticketer_data, dict):
|
|
54
|
+
return f"Ticketer at index {ticketer_idx} must be an object in the ticketer definition file"
|
|
55
|
+
|
|
56
|
+
if not ticketer_data.get("name"):
|
|
57
|
+
return (
|
|
58
|
+
f"Ticketer at index {ticketer_idx} is missing required field 'name' "
|
|
59
|
+
"in the ticketer definition file"
|
|
60
|
+
)
|
|
61
|
+
if not isinstance(ticketer_data["name"], str):
|
|
62
|
+
return (
|
|
63
|
+
f"Ticketer at index {ticketer_idx}: 'name' must be a string "
|
|
64
|
+
"in the ticketer definition file"
|
|
65
|
+
)
|
|
66
|
+
if len(ticketer_data["name"]) > MAX_TICKETER_NAME_LENGTH:
|
|
67
|
+
return (
|
|
68
|
+
f"Ticketer at index {ticketer_idx}: 'name' must be less than "
|
|
69
|
+
f"{MAX_TICKETER_NAME_LENGTH} characters in the ticketer definition file"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if not ticketer_data.get("ticketer_type"):
|
|
73
|
+
return (
|
|
74
|
+
f"Ticketer at index {ticketer_idx} is missing required field 'ticketer_type' "
|
|
75
|
+
"in the ticketer definition file"
|
|
76
|
+
)
|
|
77
|
+
if not isinstance(ticketer_data["ticketer_type"], str):
|
|
78
|
+
return (
|
|
79
|
+
f"Ticketer at index {ticketer_idx}: 'ticketer_type' must be a string "
|
|
80
|
+
"in the ticketer definition file"
|
|
81
|
+
)
|
|
82
|
+
if ticketer_data["ticketer_type"] not in AVAILABLE_TICKETER_TYPES:
|
|
83
|
+
return (
|
|
84
|
+
f"Ticketer at index {ticketer_idx}: 'ticketer_type' must be one of: "
|
|
85
|
+
f"{', '.join(AVAILABLE_TICKETER_TYPES)} in the ticketer definition file"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if not ticketer_data.get("config"):
|
|
89
|
+
return (
|
|
90
|
+
f"Ticketer at index {ticketer_idx} is missing required field 'config' "
|
|
91
|
+
"in the ticketer definition file"
|
|
92
|
+
)
|
|
93
|
+
if not isinstance(ticketer_data["config"], dict):
|
|
94
|
+
return (
|
|
95
|
+
f"Ticketer at index {ticketer_idx}: 'config' must be an object "
|
|
96
|
+
"in the ticketer definition file"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
config = ticketer_data["config"]
|
|
100
|
+
allowed_fields = set(REQUIRED_CONFIG_FIELDS + OPTIONAL_CONFIG_FIELDS)
|
|
101
|
+
|
|
102
|
+
for field in REQUIRED_CONFIG_FIELDS:
|
|
103
|
+
if field not in config:
|
|
104
|
+
return (
|
|
105
|
+
f"Ticketer at index {ticketer_idx}: 'config' is missing required field "
|
|
106
|
+
f"'{field}' in the ticketer definition file"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
for field, value in config.items():
|
|
110
|
+
if field not in allowed_fields:
|
|
111
|
+
return (
|
|
112
|
+
f"Ticketer at index {ticketer_idx}: 'config.{field}' is not a recognized "
|
|
113
|
+
"field in the ticketer definition file"
|
|
114
|
+
)
|
|
115
|
+
if not isinstance(value, str):
|
|
116
|
+
return (
|
|
117
|
+
f"Ticketer at index {ticketer_idx}: 'config.{field}' must be a string "
|
|
118
|
+
"in the ticketer definition file"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if not config["base_url"].strip():
|
|
122
|
+
return (
|
|
123
|
+
f"Ticketer at index {ticketer_idx}: 'config.base_url' must not be empty "
|
|
124
|
+
"in the ticketer definition file"
|
|
125
|
+
)
|
|
126
|
+
if not URL_PATTERN.match(config["base_url"].strip()):
|
|
127
|
+
return (
|
|
128
|
+
f"Ticketer at index {ticketer_idx}: 'config.base_url' must be a valid URL "
|
|
129
|
+
"starting with http:// or https:// in the ticketer definition file"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if not config["api_token"].strip():
|
|
133
|
+
return (
|
|
134
|
+
f"Ticketer at index {ticketer_idx}: 'config.api_token' must not be empty "
|
|
135
|
+
"in the ticketer definition file"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
skip_hmac = skip_webhook_hmac_enabled(config.get("skip_webhook_hmac", ""))
|
|
139
|
+
webhook_secret = config.get("webhook_secret", "").strip()
|
|
140
|
+
if not skip_hmac and not webhook_secret:
|
|
141
|
+
return (
|
|
142
|
+
f"Ticketer at index {ticketer_idx}: 'config.webhook_secret' is required unless "
|
|
143
|
+
"'config.skip_webhook_hmac' is set to true, 1 or yes in the ticketer definition file"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
for field in OPTIONAL_CONFIG_FIELDS:
|
|
147
|
+
if field in config and field != "skip_webhook_hmac" and not config[field].strip():
|
|
148
|
+
if field == "webhook_secret" and skip_hmac:
|
|
149
|
+
continue
|
|
150
|
+
if field in ("project_uuid", "project_name") and config[field] == "":
|
|
151
|
+
continue
|
|
152
|
+
return (
|
|
153
|
+
f"Ticketer at index {ticketer_idx}: 'config.{field}' must not be empty "
|
|
154
|
+
"in the ticketer definition file"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def load_ticketer_definition(path) -> tuple[Any, Optional[Exception]]:
|
|
161
|
+
"""
|
|
162
|
+
Loads a ticketer definition from a YAML file.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
path (str): Path to the ticketer definition file
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
tuple: (parsed_data, error) where error is None if successful
|
|
169
|
+
"""
|
|
170
|
+
data, error = load_yaml_file(path)
|
|
171
|
+
if error:
|
|
172
|
+
return None, error
|
|
173
|
+
|
|
174
|
+
if not data:
|
|
175
|
+
return None, Exception("Empty definition file")
|
|
176
|
+
|
|
177
|
+
return data, None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|