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.
Files changed (54) hide show
  1. {weni_cli-3.7.2 → weni_cli-3.8.0}/PKG-INFO +1 -1
  2. {weni_cli-3.7.2 → weni_cli-3.8.0}/pyproject.toml +1 -1
  3. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/cli.py +21 -0
  4. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/cli_client.py +18 -0
  5. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/tests/test_cli_client.py +87 -0
  6. weni_cli-3.8.0/weni_cli/commands/ticketer_create.py +63 -0
  7. weni_cli-3.8.0/weni_cli/validators/tests/test_ticketer_definition.py +206 -0
  8. weni_cli-3.8.0/weni_cli/validators/ticketer_definition.py +177 -0
  9. {weni_cli-3.7.2 → weni_cli-3.8.0}/README.md +0 -0
  10. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/__init__.py +0 -0
  11. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/auth.py +0 -0
  12. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/__init__.py +0 -0
  13. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/common.py +0 -0
  14. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/response_handlers/__init__.py +0 -0
  15. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/response_handlers/handlers.py +0 -0
  16. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/tests/__init__.py +0 -0
  17. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/tests/test_weni_client.py +0 -0
  18. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/clients/weni_client.py +0 -0
  19. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/channel_create.py +0 -0
  20. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/eval_init.py +0 -0
  21. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/eval_run.py +0 -0
  22. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/init.py +0 -0
  23. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/login.py +0 -0
  24. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/logs.py +0 -0
  25. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/project_current.py +0 -0
  26. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/project_list.py +0 -0
  27. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/project_push.py +0 -0
  28. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/project_use.py +0 -0
  29. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/commands/run.py +0 -0
  30. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/formatter/__init__.py +0 -0
  31. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/formatter/formatter.py +0 -0
  32. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/formatter/tests/test_formatter.py +0 -0
  33. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/handler.py +0 -0
  34. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/packager/__init__.py +0 -0
  35. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/packager/loader.py +0 -0
  36. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/packager/packager.py +0 -0
  37. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/packager/tests/__init__.py +0 -0
  38. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/packager/tests/test_loader.py +0 -0
  39. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/packager/tests/test_packager.py +0 -0
  40. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/spinner/__init__.py +0 -0
  41. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/spinner/spinners.py +0 -0
  42. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/store.py +0 -0
  43. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/utils.py +0 -0
  44. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/__init__.py +0 -0
  45. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/agent_definition.py +0 -0
  46. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/channel_definition.py +0 -0
  47. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/source.py +0 -0
  48. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/tests/__init__.py +0 -0
  49. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/tests/conftest.py +0 -0
  50. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/tests/test_active_test_definition.py +0 -0
  51. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/tests/test_channel_definition.py +0 -0
  52. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/tests/test_definition.py +0 -0
  53. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/validators/tests/test_source.py +0 -0
  54. {weni_cli-3.7.2 → weni_cli-3.8.0}/weni_cli/wsgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: weni-cli
3
- Version: 3.7.2
3
+ Version: 3.8.0
4
4
  Summary:
5
5
  Author: Paulo Bernardo
6
6
  Author-email: paulo.bernardo@weni.ai
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "weni-cli"
3
- version = "3.7.2"
3
+ version = "3.8.0"
4
4
  description = ""
5
5
  authors = ["Paulo Bernardo <paulo.bernardo@weni.ai>", "Matheus Leal <matheus.cardoso@vtex.com>"]
6
6
  readme = "README.md"
@@ -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