argus-alm 0.14.1__py3-none-any.whl → 0.15.1__py3-none-any.whl

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 (118) hide show
  1. argus/_version.py +21 -0
  2. argus/backend/.gitkeep +0 -0
  3. argus/backend/__init__.py +0 -0
  4. argus/backend/cli.py +57 -0
  5. argus/backend/controller/__init__.py +0 -0
  6. argus/backend/controller/admin.py +20 -0
  7. argus/backend/controller/admin_api.py +355 -0
  8. argus/backend/controller/api.py +589 -0
  9. argus/backend/controller/auth.py +67 -0
  10. argus/backend/controller/client_api.py +109 -0
  11. argus/backend/controller/main.py +316 -0
  12. argus/backend/controller/notification_api.py +72 -0
  13. argus/backend/controller/notifications.py +13 -0
  14. argus/backend/controller/planner_api.py +194 -0
  15. argus/backend/controller/team.py +129 -0
  16. argus/backend/controller/team_ui.py +19 -0
  17. argus/backend/controller/testrun_api.py +513 -0
  18. argus/backend/controller/view_api.py +188 -0
  19. argus/backend/controller/views_widgets/__init__.py +0 -0
  20. argus/backend/controller/views_widgets/graphed_stats.py +54 -0
  21. argus/backend/controller/views_widgets/graphs.py +68 -0
  22. argus/backend/controller/views_widgets/highlights.py +135 -0
  23. argus/backend/controller/views_widgets/nemesis_stats.py +26 -0
  24. argus/backend/controller/views_widgets/summary.py +43 -0
  25. argus/backend/db.py +98 -0
  26. argus/backend/error_handlers.py +41 -0
  27. argus/backend/events/event_processors.py +34 -0
  28. argus/backend/models/__init__.py +0 -0
  29. argus/backend/models/argus_ai.py +24 -0
  30. argus/backend/models/github_issue.py +60 -0
  31. argus/backend/models/plan.py +24 -0
  32. argus/backend/models/result.py +187 -0
  33. argus/backend/models/runtime_store.py +58 -0
  34. argus/backend/models/view_widgets.py +25 -0
  35. argus/backend/models/web.py +403 -0
  36. argus/backend/plugins/__init__.py +0 -0
  37. argus/backend/plugins/core.py +248 -0
  38. argus/backend/plugins/driver_matrix_tests/controller.py +66 -0
  39. argus/backend/plugins/driver_matrix_tests/model.py +429 -0
  40. argus/backend/plugins/driver_matrix_tests/plugin.py +21 -0
  41. argus/backend/plugins/driver_matrix_tests/raw_types.py +62 -0
  42. argus/backend/plugins/driver_matrix_tests/service.py +61 -0
  43. argus/backend/plugins/driver_matrix_tests/udt.py +42 -0
  44. argus/backend/plugins/generic/model.py +86 -0
  45. argus/backend/plugins/generic/plugin.py +15 -0
  46. argus/backend/plugins/generic/types.py +14 -0
  47. argus/backend/plugins/loader.py +39 -0
  48. argus/backend/plugins/sct/controller.py +224 -0
  49. argus/backend/plugins/sct/plugin.py +37 -0
  50. argus/backend/plugins/sct/resource_setup.py +177 -0
  51. argus/backend/plugins/sct/service.py +682 -0
  52. argus/backend/plugins/sct/testrun.py +288 -0
  53. argus/backend/plugins/sct/udt.py +100 -0
  54. argus/backend/plugins/sirenada/model.py +118 -0
  55. argus/backend/plugins/sirenada/plugin.py +16 -0
  56. argus/backend/service/admin.py +26 -0
  57. argus/backend/service/argus_service.py +696 -0
  58. argus/backend/service/build_system_monitor.py +185 -0
  59. argus/backend/service/client_service.py +127 -0
  60. argus/backend/service/event_service.py +18 -0
  61. argus/backend/service/github_service.py +233 -0
  62. argus/backend/service/jenkins_service.py +269 -0
  63. argus/backend/service/notification_manager.py +159 -0
  64. argus/backend/service/planner_service.py +608 -0
  65. argus/backend/service/release_manager.py +229 -0
  66. argus/backend/service/results_service.py +690 -0
  67. argus/backend/service/stats.py +610 -0
  68. argus/backend/service/team_manager_service.py +82 -0
  69. argus/backend/service/test_lookup.py +172 -0
  70. argus/backend/service/testrun.py +489 -0
  71. argus/backend/service/user.py +308 -0
  72. argus/backend/service/views.py +219 -0
  73. argus/backend/service/views_widgets/__init__.py +0 -0
  74. argus/backend/service/views_widgets/graphed_stats.py +180 -0
  75. argus/backend/service/views_widgets/highlights.py +374 -0
  76. argus/backend/service/views_widgets/nemesis_stats.py +34 -0
  77. argus/backend/template_filters.py +27 -0
  78. argus/backend/tests/__init__.py +0 -0
  79. argus/backend/tests/client_service/__init__.py +0 -0
  80. argus/backend/tests/client_service/test_submit_results.py +79 -0
  81. argus/backend/tests/conftest.py +180 -0
  82. argus/backend/tests/results_service/__init__.py +0 -0
  83. argus/backend/tests/results_service/test_best_results.py +178 -0
  84. argus/backend/tests/results_service/test_cell.py +65 -0
  85. argus/backend/tests/results_service/test_chartjs_additional_functions.py +259 -0
  86. argus/backend/tests/results_service/test_create_chartjs.py +220 -0
  87. argus/backend/tests/results_service/test_result_metadata.py +100 -0
  88. argus/backend/tests/results_service/test_results_service.py +203 -0
  89. argus/backend/tests/results_service/test_validation_rules.py +213 -0
  90. argus/backend/tests/view_widgets/__init__.py +0 -0
  91. argus/backend/tests/view_widgets/test_highlights_api.py +532 -0
  92. argus/backend/util/common.py +65 -0
  93. argus/backend/util/config.py +38 -0
  94. argus/backend/util/encoders.py +56 -0
  95. argus/backend/util/logsetup.py +80 -0
  96. argus/backend/util/module_loaders.py +30 -0
  97. argus/backend/util/send_email.py +91 -0
  98. argus/client/base.py +1 -3
  99. argus/client/driver_matrix_tests/cli.py +19 -10
  100. argus/client/generic/cli.py +4 -2
  101. argus/client/generic/client.py +1 -0
  102. argus/client/generic_result.py +48 -9
  103. argus/client/sct/client.py +1 -3
  104. argus/client/sirenada/client.py +4 -1
  105. argus/client/tests/__init__.py +0 -0
  106. argus/client/tests/conftest.py +19 -0
  107. argus/client/tests/test_package.py +45 -0
  108. argus/client/tests/test_results.py +224 -0
  109. argus/common/sct_types.py +3 -0
  110. argus/common/sirenada_types.py +1 -1
  111. {argus_alm-0.14.1.dist-info → argus_alm-0.15.1.dist-info}/METADATA +43 -19
  112. argus_alm-0.15.1.dist-info/RECORD +122 -0
  113. {argus_alm-0.14.1.dist-info → argus_alm-0.15.1.dist-info}/WHEEL +2 -1
  114. argus_alm-0.15.1.dist-info/entry_points.txt +3 -0
  115. argus_alm-0.15.1.dist-info/top_level.txt +1 -0
  116. argus_alm-0.14.1.dist-info/RECORD +0 -20
  117. argus_alm-0.14.1.dist-info/entry_points.txt +0 -4
  118. {argus_alm-0.14.1.dist-info → argus_alm-0.15.1.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,532 @@
1
+ import json
2
+ import time
3
+ from datetime import datetime, UTC
4
+ from unittest.mock import patch
5
+ from uuid import uuid4, UUID
6
+
7
+ from flask import g
8
+
9
+ from argus.backend.models.view_widgets import WidgetHighlights, WidgetComment
10
+
11
+
12
+ @patch("argus.backend.service.views_widgets.highlights.HighlightsService._send_highlight_notifications")
13
+ def test_create_highlight_should_return_created_highlight(notifications, flask_client):
14
+ view_id = str(uuid4())
15
+ now = datetime.now(UTC)
16
+ response = flask_client.post(
17
+ "/api/v1/views/widgets/highlights/create",
18
+ data=json.dumps({"view_id": view_id, "index": 0, "content": "Highlight content", "is_task": False}),
19
+ content_type="application/json",
20
+ )
21
+
22
+ assert response.status_code == 200, response.text
23
+ assert response.json["status"] == "ok"
24
+ assert response.json["response"]["view_id"] == view_id
25
+ assert response.json["response"]["content"] == "Highlight content"
26
+ assert response.json["response"]["archived_at"] == 0
27
+ assert response.json["response"]["comments_count"] == 0
28
+ assert response.json["response"]["creator_id"] == str(g.user.id)
29
+ assert response.json["response"]["created_at"] > now.timestamp()
30
+ assert "completed" not in response.json["response"]
31
+ assert "assignee_id" not in response.json["response"]
32
+
33
+
34
+ @patch("argus.backend.service.views_widgets.highlights.HighlightsService._send_highlight_notifications")
35
+ def test_create_action_item_should_return_created_action_item(notifications, flask_client):
36
+ view_id = str(uuid4())
37
+ now = datetime.now(UTC)
38
+ response = flask_client.post(
39
+ "/api/v1/views/widgets/highlights/create",
40
+ data=json.dumps({"view_id": view_id, "index": 0, "content": "Action item content", "is_task": True}),
41
+ content_type="application/json",
42
+ )
43
+
44
+ assert response.status_code == 200, response.text
45
+ assert response.json["status"] == "ok"
46
+ assert response.json["response"]["view_id"] == view_id
47
+ assert response.json["response"]["content"] == "Action item content"
48
+ assert response.json["response"]["completed"] is False
49
+ assert response.json["response"]["assignee_id"] is None
50
+ assert response.json["response"]["archived_at"] == 0
51
+ assert response.json["response"]["comments_count"] == 0
52
+ assert response.json["response"]["creator_id"] == str(g.user.id)
53
+ assert response.json["response"]["created_at"] > now.timestamp()
54
+
55
+
56
+ def test_get_highlights_should_return_highlights_and_action_items(flask_client):
57
+ view_id = str(uuid4())
58
+ creator_id = g.user.id
59
+
60
+ highlight_entry = WidgetHighlights(
61
+ view_id=UUID(view_id),
62
+ index=0,
63
+ created_at=datetime.now(UTC),
64
+ creator_id=creator_id,
65
+ content="Test highlight",
66
+ completed=None,
67
+ comments_count=0,
68
+ )
69
+ highlight_entry.save()
70
+
71
+ action_item_entry = WidgetHighlights(
72
+ view_id=UUID(view_id),
73
+ index=0,
74
+ created_at=datetime.now(UTC),
75
+ creator_id=creator_id,
76
+ content="Test action item",
77
+ completed=False,
78
+ comments_count=0,
79
+ )
80
+ action_item_entry.save()
81
+
82
+ # Add small delay to ensure can be read (test was flaky)
83
+ time.sleep(0.05) # 50ms delay
84
+
85
+ response = flask_client.get(f"/api/v1/views/widgets/highlights?view_id={view_id}&index=0")
86
+
87
+ assert response.status_code == 200
88
+ assert response.json["status"] == "ok"
89
+ highlights = response.json["response"]["highlights"]
90
+ action_items = response.json["response"]["action_items"]
91
+
92
+ assert len(highlights) == 1
93
+ assert len(action_items) == 1
94
+ assert highlights[0]["content"] == "Test highlight"
95
+ assert action_items[0]["content"] == "Test action item"
96
+
97
+
98
+ def test_archive_highlight_should_mark_highlight_as_archived(flask_client):
99
+ view_id = str(uuid4())
100
+ created_at = datetime.now(UTC)
101
+ creator_id = g.user.id
102
+ highlight_entry = WidgetHighlights(
103
+ view_id=UUID(view_id),
104
+ index=0,
105
+ created_at=created_at,
106
+ creator_id=creator_id,
107
+ content="Test highlight",
108
+ completed=None,
109
+ comments_count=0,
110
+ archived_at=datetime.fromtimestamp(0, tz=UTC),
111
+ )
112
+ highlight_entry.save()
113
+
114
+ response = flask_client.post(
115
+ "/api/v1/views/widgets/highlights/archive",
116
+ data=json.dumps(
117
+ {
118
+ "view_id": view_id,
119
+ "index": 0,
120
+ "created_at": created_at.timestamp(),
121
+ }
122
+ ),
123
+ content_type="application/json",
124
+ )
125
+ assert response.status_code == 200
126
+ assert response.json["status"] == "ok"
127
+
128
+ archived_entry = WidgetHighlights.objects(view_id=UUID(view_id), index=0, created_at=created_at).first()
129
+ assert archived_entry.archived_at.replace(tzinfo=UTC) > created_at
130
+
131
+
132
+ def test_unarchive_highlight_should_unmark_highlight_from_archived(flask_client):
133
+ view_id = str(uuid4())
134
+ created_at = datetime.now(UTC)
135
+ creator_id = g.user.id
136
+ archived_time = datetime.now(UTC)
137
+ highlight_entry = WidgetHighlights(
138
+ view_id=UUID(view_id),
139
+ index=0,
140
+ created_at=created_at,
141
+ creator_id=creator_id,
142
+ content="Test highlight",
143
+ completed=None,
144
+ comments_count=0,
145
+ archived_at=archived_time,
146
+ )
147
+ highlight_entry.save()
148
+
149
+ response = flask_client.post(
150
+ "/api/v1/views/widgets/highlights/unarchive",
151
+ data=json.dumps(
152
+ {
153
+ "view_id": view_id,
154
+ "index": 0,
155
+ "created_at": created_at.timestamp(),
156
+ }
157
+ ),
158
+ content_type="application/json",
159
+ )
160
+ assert response.status_code == 200
161
+ assert response.json["status"] == "ok"
162
+
163
+ unarchived_entry = WidgetHighlights.objects(view_id=UUID(view_id), index=0, created_at=created_at).first()
164
+ assert unarchived_entry.archived_at.replace(tzinfo=UTC) == datetime.fromtimestamp(0, tz=UTC)
165
+
166
+
167
+ @patch("argus.backend.service.views_widgets.highlights.HighlightsService._send_highlight_notifications")
168
+ def test_update_highlight_should_update_content_for_creator(notifications, flask_client):
169
+ view_id = str(uuid4())
170
+ created_at = datetime.now(UTC)
171
+ creator_id = g.user.id
172
+ original_content = "Original content"
173
+ updated_content = "Updated content"
174
+
175
+ highlight_entry = WidgetHighlights(
176
+ view_id=UUID(view_id),
177
+ index=0,
178
+ created_at=created_at,
179
+ creator_id=creator_id,
180
+ content=original_content,
181
+ completed=None,
182
+ comments_count=0,
183
+ archived_at=datetime.fromtimestamp(0, tz=UTC),
184
+ )
185
+ highlight_entry.save()
186
+
187
+ response = flask_client.post(
188
+ "/api/v1/views/widgets/highlights/update",
189
+ data=json.dumps({
190
+ "view_id": view_id,
191
+ "index": 0,
192
+ "created_at": created_at.timestamp(),
193
+ "content": updated_content,
194
+ }),
195
+ content_type="application/json",
196
+ )
197
+ assert response.status_code == 200
198
+ assert response.json["status"] == "ok"
199
+ assert response.json["response"]["content"] == updated_content
200
+
201
+ updated_entry = WidgetHighlights.objects(view_id=UUID(view_id), index=0, created_at=created_at).first()
202
+ assert updated_entry.content == updated_content
203
+
204
+
205
+ def test_update_highlight_should_forbid_non_creator(flask_client):
206
+ view_id = str(uuid4())
207
+ created_at = datetime.now(UTC)
208
+ creator_id = uuid4() # Different from the logged-in user
209
+ original_content = "Original content"
210
+ malicious_content = "Malicious update"
211
+
212
+ highlight_entry = WidgetHighlights(
213
+ view_id=UUID(view_id),
214
+ index=0,
215
+ created_at=created_at,
216
+ creator_id=creator_id,
217
+ content=original_content,
218
+ completed=None,
219
+ comments_count=0,
220
+ archived_at=datetime.fromtimestamp(0, tz=UTC),
221
+ )
222
+ highlight_entry.save()
223
+
224
+ response = flask_client.post(
225
+ "/api/v1/views/widgets/highlights/update",
226
+ data=json.dumps({
227
+ "view_id": view_id,
228
+ "index": 0,
229
+ "created_at": created_at.timestamp(),
230
+ "content": malicious_content,
231
+ }),
232
+ content_type="application/json",
233
+ )
234
+ assert response.status_code == 200, response.text
235
+ assert response.json["status"] == "error"
236
+ assert response.json["response"]["exception"] == "Forbidden"
237
+
238
+ unchanged_entry = WidgetHighlights.objects(view_id=UUID(view_id), index=0, created_at=created_at).first()
239
+ assert unchanged_entry.content == original_content
240
+
241
+
242
+ def test_set_completed_should_update_completed_status(flask_client):
243
+ view_id = str(uuid4())
244
+ created_at = datetime.now(UTC)
245
+ action_item_entry = WidgetHighlights(
246
+ view_id=UUID(view_id),
247
+ index=0,
248
+ created_at=created_at,
249
+ creator_id=g.user.id,
250
+ content="Test action item",
251
+ completed=False,
252
+ comments_count=0,
253
+ )
254
+ action_item_entry.save()
255
+
256
+ # Set completed to True
257
+ response = flask_client.post(
258
+ "/api/v1/views/widgets/highlights/set_completed",
259
+ data=json.dumps({
260
+ "view_id": view_id,
261
+ "index": 0,
262
+ "created_at": created_at.timestamp(),
263
+ "completed": True
264
+ }),
265
+ content_type="application/json",
266
+ )
267
+ assert response.status_code == 200
268
+ assert response.json["status"] == "ok"
269
+ assert response.json["response"]["completed"] is True
270
+
271
+ # Set completed to False
272
+ response = flask_client.post(
273
+ "/api/v1/views/widgets/highlights/set_completed",
274
+ data=json.dumps({
275
+ "view_id": view_id,
276
+ "index": 0,
277
+ "created_at": created_at.timestamp(),
278
+ "completed": False
279
+ }),
280
+ content_type="application/json",
281
+ )
282
+ assert response.status_code == 200
283
+ assert response.json["response"]["completed"] is False
284
+
285
+
286
+ def test_set_completed_should_not_work_for_highlight(flask_client):
287
+ view_id = str(uuid4())
288
+ created_at = datetime.now(UTC)
289
+ highlight_entry = WidgetHighlights(
290
+ view_id=UUID(view_id),
291
+ index=0,
292
+ created_at=created_at,
293
+ creator_id=g.user.id,
294
+ content="Test highlight",
295
+ completed=None,
296
+ comments_count=0,
297
+ )
298
+ highlight_entry.save()
299
+
300
+ response = flask_client.post(
301
+ "/api/v1/views/widgets/highlights/set_completed",
302
+ data=json.dumps({
303
+ "view_id": view_id,
304
+ "index": 0,
305
+ "created_at": created_at.timestamp(),
306
+ "completed": True
307
+ }),
308
+ content_type="application/json",
309
+ )
310
+ assert response.status_code == 200, response.text
311
+ assert response.json["status"] == "error"
312
+ assert response.json["response"]["exception"] == "NotFound"
313
+
314
+
315
+ @patch("argus.backend.controller.views_widgets.highlights.HighlightsService.send_action_notification")
316
+ def test_set_assignee_should_set_assignee_for_action_item(notification, flask_client):
317
+ view_id = str(uuid4())
318
+ created_at = datetime.now(UTC)
319
+ action_item_entry = WidgetHighlights(
320
+ view_id=UUID(view_id),
321
+ index=0,
322
+ created_at=created_at,
323
+ creator_id=g.user.id,
324
+ content="Test action item",
325
+ completed=False,
326
+ comments_count=0,
327
+ )
328
+ action_item_entry.save()
329
+
330
+ new_assignee_id = str(uuid4())
331
+
332
+ response = flask_client.post(
333
+ "/api/v1/views/widgets/highlights/set_assignee",
334
+ data=json.dumps({
335
+ "view_id": view_id,
336
+ "index": 0,
337
+ "created_at": created_at.timestamp(),
338
+ "assignee_id": new_assignee_id
339
+ }),
340
+ content_type="application/json",
341
+ )
342
+ assert response.status_code == 200
343
+ assert response.json["status"] == "ok"
344
+ assert response.json["response"]["assignee_id"] == new_assignee_id
345
+ assert notification.call_count == 1
346
+
347
+ updated_entry = WidgetHighlights.objects(view_id=UUID(view_id), index=0, created_at=created_at).first()
348
+ assert str(updated_entry.assignee_id) == new_assignee_id
349
+
350
+
351
+ def test_set_assignee_should_not_work_for_highlight(flask_client):
352
+ view_id = str(uuid4())
353
+ created_at = datetime.now(UTC)
354
+ highlight_entry = WidgetHighlights(
355
+ view_id=UUID(view_id),
356
+ index=0,
357
+ created_at=created_at,
358
+ creator_id=g.user.id,
359
+ content="Test highlight",
360
+ completed=None,
361
+ comments_count=0,
362
+ )
363
+ highlight_entry.save()
364
+
365
+ new_assignee_id = str(uuid4())
366
+
367
+ response = flask_client.post(
368
+ "/api/v1/views/widgets/highlights/set_assignee",
369
+ data=json.dumps({
370
+ "view_id": view_id,
371
+ "index": 0,
372
+ "created_at": created_at.timestamp(),
373
+ "assignee_id": new_assignee_id
374
+ }),
375
+ content_type="application/json",
376
+ )
377
+ assert response.status_code == 200, response.text
378
+ assert response.json["status"] == "error"
379
+ assert response.json["response"]["exception"] == "NotFound"
380
+
381
+
382
+ @patch("argus.backend.service.views_widgets.highlights.HighlightsService._send_highlight_notifications")
383
+ def test_create_comment_should_increment_comments_count(notification, flask_client):
384
+ view_id = str(uuid4())
385
+ highlight_created_at = datetime.now(UTC)
386
+ highlight_entry = WidgetHighlights(
387
+ view_id=UUID(view_id),
388
+ index=0,
389
+ created_at=highlight_created_at,
390
+ creator_id=g.user.id,
391
+ content="Test highlight",
392
+ completed=None,
393
+ comments_count=0,
394
+ )
395
+ highlight_entry.save()
396
+
397
+ response = flask_client.post(
398
+ "/api/v1/views/widgets/highlights/comments/create",
399
+ data=json.dumps({
400
+ "view_id": view_id,
401
+ "index": 0,
402
+ "highlight_created_at": highlight_created_at.timestamp(),
403
+ "content": "Test comment",
404
+ }),
405
+ content_type="application/json",
406
+ )
407
+ assert response.status_code == 200
408
+ assert response.json["status"] == "ok"
409
+ assert response.json["response"]["content"] == "Test comment"
410
+
411
+ updated_highlight = WidgetHighlights.objects(view_id=UUID(
412
+ view_id), index=0, created_at=highlight_created_at).first()
413
+ assert updated_highlight.comments_count == 1
414
+
415
+
416
+ def test_delete_comment_should_decrement_comments_count(flask_client):
417
+ view_id = str(uuid4())
418
+ highlight_created_at = datetime.now(UTC)
419
+ comment_created_at = datetime.now(UTC)
420
+ highlight_entry = WidgetHighlights(
421
+ view_id=UUID(view_id),
422
+ index=0,
423
+ created_at=highlight_created_at,
424
+ creator_id=g.user.id,
425
+ content="Test highlight",
426
+ completed=None,
427
+ comments_count=1,
428
+ )
429
+ highlight_entry.save()
430
+ comment_entry = WidgetComment(
431
+ view_id=UUID(view_id),
432
+ index=0,
433
+ highlight_at=highlight_created_at,
434
+ created_at=comment_created_at,
435
+ creator_id=g.user.id,
436
+ content="Test comment",
437
+ )
438
+ comment_entry.save()
439
+
440
+ response = flask_client.post(
441
+ "/api/v1/views/widgets/highlights/comments/delete",
442
+ data=json.dumps({
443
+ "view_id": view_id,
444
+ "index": 0,
445
+ "highlight_created_at": highlight_created_at.timestamp(),
446
+ "created_at": comment_created_at.timestamp(),
447
+ }),
448
+ content_type="application/json",
449
+ )
450
+ assert response.status_code == 200
451
+ assert response.json["status"] == "ok"
452
+
453
+ updated_highlight = WidgetHighlights.objects(view_id=UUID(
454
+ view_id), index=0, created_at=highlight_created_at).first()
455
+ assert updated_highlight.comments_count == 0
456
+
457
+
458
+ @patch("argus.backend.service.views_widgets.highlights.HighlightsService._send_highlight_notifications")
459
+ def test_update_comment_should_modify_content(notification, flask_client):
460
+ view_id = str(uuid4())
461
+ highlight_created_at = datetime.now(UTC)
462
+ comment_created_at = datetime.now(UTC)
463
+ comment_content = "Original comment"
464
+ updated_content = "Updated comment"
465
+
466
+ highlight_entry = WidgetHighlights(
467
+ view_id=UUID(view_id),
468
+ index=0,
469
+ created_at=highlight_created_at,
470
+ creator_id=g.user.id,
471
+ content="Test highlight",
472
+ completed=None,
473
+ comments_count=1,
474
+ )
475
+ highlight_entry.save()
476
+ comment_entry = WidgetComment(
477
+ view_id=UUID(view_id),
478
+ index=0,
479
+ highlight_at=highlight_created_at,
480
+ created_at=comment_created_at,
481
+ creator_id=g.user.id,
482
+ content=comment_content,
483
+ )
484
+ comment_entry.save()
485
+
486
+ response = flask_client.post(
487
+ "/api/v1/views/widgets/highlights/comments/update",
488
+ data=json.dumps({
489
+ "view_id": view_id,
490
+ "index": 0,
491
+ "highlight_created_at": highlight_created_at.timestamp(),
492
+ "created_at": comment_created_at.timestamp(),
493
+ "content": updated_content,
494
+ }),
495
+ content_type="application/json",
496
+ )
497
+ assert response.status_code == 200
498
+ assert response.json["status"] == "ok"
499
+ assert response.json["response"]["content"] == updated_content
500
+
501
+
502
+ def test_get_comments_should_return_list_of_comments(flask_client):
503
+ view_id = str(uuid4())
504
+ highlight_created_at = datetime.now(UTC)
505
+ comment_created_at = datetime.now(UTC)
506
+ highlight_entry = WidgetHighlights(
507
+ view_id=UUID(view_id),
508
+ index=0,
509
+ created_at=highlight_created_at,
510
+ creator_id=g.user.id,
511
+ content="Test highlight",
512
+ completed=None,
513
+ comments_count=1,
514
+ )
515
+ highlight_entry.save()
516
+ comment_entry = WidgetComment(
517
+ view_id=UUID(view_id),
518
+ index=0,
519
+ highlight_at=highlight_created_at,
520
+ created_at=comment_created_at,
521
+ creator_id=g.user.id,
522
+ content="Test comment",
523
+ )
524
+ comment_entry.save()
525
+
526
+ response = flask_client.get(
527
+ f"/api/v1/views/widgets/highlights/comments?view_id={view_id}&index=0&created_at={highlight_created_at.timestamp()}")
528
+ assert response.status_code == 200
529
+ assert response.json["status"] == "ok"
530
+ comments = response.json["response"]
531
+ assert len(comments) == 1
532
+ assert comments[0]["content"] == "Test comment"
@@ -0,0 +1,65 @@
1
+ from itertools import islice
2
+ import logging
3
+ from typing import Callable, Iterable
4
+ from uuid import UUID
5
+
6
+ from flask import Request, Response, g
7
+
8
+ from argus.backend.models.web import User
9
+
10
+
11
+ LOGGER = logging.getLogger(__name__)
12
+ FlaskView = Callable[..., Response]
13
+
14
+
15
+ def first(iterable, value, key: Callable = None, predicate: Callable = None):
16
+ for elem in iterable:
17
+ if predicate and predicate(elem, value):
18
+ return elem
19
+ elif key and key(elem) == value:
20
+ return elem
21
+ elif elem == value:
22
+ return elem
23
+ return None
24
+
25
+
26
+ def chunk(iterable: Iterable, slice_size=90):
27
+ it = iter(iterable)
28
+ return iter(lambda: list(islice(it, slice_size)), [])
29
+
30
+
31
+ def check_scheduled_test(test, group, testname):
32
+ return testname in (f"{group}/{test}", test)
33
+
34
+
35
+ def strip_html_tags(text: str):
36
+ return text.replace("<", "&lt;").replace(">", "&gt;")
37
+
38
+
39
+ def convert_str_list_to_uuid(lst: list[str]) -> list[UUID]:
40
+ return [UUID(s) for s in lst]
41
+
42
+
43
+ def get_payload(client_request: Request) -> dict:
44
+ if not client_request.is_json:
45
+ raise Exception(
46
+ "Content-Type mismatch, expected application/json, got:",
47
+ client_request.content_type
48
+ )
49
+ request_payload = client_request.get_json()
50
+
51
+ return request_payload
52
+
53
+
54
+ def current_user() -> User:
55
+ return g.user
56
+
57
+
58
+ def get_build_number(build_job_url: str) -> int | None:
59
+ build_number = build_job_url.rstrip("/").split("/")[-1] if build_job_url else -1
60
+ if build_number:
61
+ try:
62
+ return int(build_number)
63
+ except ValueError:
64
+ LOGGER.error("Error parsing build number from %s: got %s as build_number", build_job_url, build_number)
65
+ return None
@@ -0,0 +1,38 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ import yaml
5
+
6
+
7
+ LOGGER = logging.getLogger(__name__)
8
+
9
+
10
+ class Config:
11
+ CONFIG = None
12
+ CONFIG_PATHS = [
13
+ Path(__file__).parents[3] / "config" / "argus_web.yaml",
14
+ Path("argus_web.yaml"),
15
+ Path("../config/argus_web.yaml"),
16
+
17
+ ]
18
+
19
+ @classmethod
20
+ def locate_argus_web_config(cls) -> Path:
21
+ for config in cls.CONFIG_PATHS:
22
+ if config.exists():
23
+ return config
24
+ else:
25
+ LOGGER.debug("Tried %s as config, not found.", config)
26
+
27
+ raise Exception("Failed to locate web application config file!")
28
+
29
+ @classmethod
30
+ def load_yaml_config(cls) -> dict:
31
+ if cls.CONFIG:
32
+ return cls.CONFIG
33
+ path = cls.locate_argus_web_config()
34
+ with open(path, "rt", encoding="utf-8") as file:
35
+ config = yaml.safe_load(file)
36
+
37
+ cls.CONFIG = config
38
+ return config
@@ -0,0 +1,56 @@
1
+ from datetime import datetime
2
+ import logging
3
+ from json.encoder import JSONEncoder
4
+ from uuid import UUID
5
+
6
+ from flask.json.provider import DefaultJSONProvider
7
+ import cassandra.cqlengine.usertype as ut
8
+ import cassandra.cqlengine.models as m
9
+
10
+
11
+ LOGGER = logging.getLogger(__name__)
12
+
13
+
14
+ class ArgusJSONEncoder(JSONEncoder):
15
+ def default(self, o):
16
+ match o:
17
+ case UUID():
18
+ return str(o)
19
+ case ut.UserType():
20
+ return dict(o.items())
21
+ case m.Model():
22
+ return dict(o.items())
23
+ case datetime():
24
+ return o.strftime("%Y-%m-%dT%H:%M:%SZ")
25
+ case _:
26
+ return super().default(o)
27
+
28
+
29
+ class ArgusJSONProvider(DefaultJSONProvider):
30
+
31
+ @staticmethod
32
+ def process_nested_dicts(o: dict):
33
+ for k, v in o.items():
34
+ if isinstance(v, dict):
35
+ o[k] = {str(key): val for key, val in v.items()}
36
+ return o
37
+
38
+ @classmethod
39
+ def default(cls, o):
40
+ match o:
41
+ case UUID():
42
+ return str(o)
43
+ case ut.UserType():
44
+ o = {str(k): v for k, v in o.items()}
45
+ o = cls.process_nested_dicts(o)
46
+ return o
47
+ case m.Model():
48
+ o = {str(k): v for k, v in o.items()}
49
+ o = cls.process_nested_dicts(o)
50
+ return o
51
+ case dict():
52
+ return {str(k): v for k, v in o.items()}
53
+ case datetime():
54
+ return o.strftime("%Y-%m-%dT%H:%M:%SZ")
55
+ case _:
56
+ return super().default(o)