View | Details | Raw Unified | Return to bug 283360 | Differences between
and this patch

Collapse All | Expand All

(-)b/devel/py-proxmoxer/Makefile (-1 / +4 lines)
Lines 17-28 RUN_DEPENDS= ${PYTHON_PKGNAMEPREFIX}requests>=2.0.0:www/py-requests@${PY_FLAVOR} Link Here
17
TEST_DEPENDS=	${PYTHON_PKGNAMEPREFIX}coveralls>0:devel/py-coveralls@${PY_FLAVOR} \
17
TEST_DEPENDS=	${PYTHON_PKGNAMEPREFIX}coveralls>0:devel/py-coveralls@${PY_FLAVOR} \
18
		${PYTHON_PKGNAMEPREFIX}openssh-wrapper>0:security/py-openssh-wrapper@${PY_FLAVOR} \
18
		${PYTHON_PKGNAMEPREFIX}openssh-wrapper>0:security/py-openssh-wrapper@${PY_FLAVOR} \
19
		${PYTHON_PKGNAMEPREFIX}paramiko>0:security/py-paramiko@${PY_FLAVOR} \
19
		${PYTHON_PKGNAMEPREFIX}paramiko>0:security/py-paramiko@${PY_FLAVOR} \
20
		${PYTHON_PKGNAMEPREFIX}requests-toolbelt>0:www/py-requests-toolbelt@${PY_FLAVOR} \
20
		${PYTHON_PKGNAMEPREFIX}responses>0:devel/py-responses@${PY_FLAVOR}
21
		${PYTHON_PKGNAMEPREFIX}responses>0:devel/py-responses@${PY_FLAVOR}
21
22
22
USES=		python
23
USES=		python
23
USE_PYTHON=	autoplist pep517 pytest
24
USE_PYTHON=	autoplist pep517 pytest
25
BINARY_ALIAS=	python3=${PYTHON_CMD}
24
26
25
TESTING_UNSAFE=	https://github.com/proxmoxer/proxmoxer/issues/195
27
EXTRA_PATCHES=	${FILESDIR}/extra-patch-tests # Sync PYPI tests with GH released version
28
#TESTING_UNSAFE=	https://github.com/proxmoxer/proxmoxer/issues/195
26
29
27
NO_ARCH=	yes
30
NO_ARCH=	yes
28
31
(-)b/devel/py-proxmoxer/files/extra-patch-tests (+1642 lines)
Added Link Here
1
Sync PYPI tests with GH released version
2
3
diff -ruN tests/__init__.py proxmoxer-2.2.0/tests/__init__.py
4
--- tests/__init__.py	1970-01-01 01:00:00.000000000 +0100
5
+++ proxmoxer-2.2.0/tests/__init__.py	2024-12-15 02:12:42.000000000 +0000
6
@@ -0,0 +1,3 @@
7
+__author__ = "John Hollowell"
8
+__copyright__ = "(c) John Hollowell 2022"
9
+__license__ = "MIT"
10
diff -ruN tests/api_mock.py proxmoxer-2.2.0/tests/api_mock.py
11
--- tests/api_mock.py	1970-01-01 01:00:00.000000000 +0100
12
+++ proxmoxer-2.2.0/tests/api_mock.py	2024-12-15 02:12:42.000000000 +0000
13
@@ -0,0 +1,360 @@
14
+__author__ = "John Hollowell"
15
+__copyright__ = "(c) John Hollowell 2022"
16
+__license__ = "MIT"
17
+
18
+import json
19
+import re
20
+from urllib.parse import parse_qsl, urlparse
21
+
22
+import pytest
23
+import responses
24
+from requests_toolbelt import MultipartEncoder
25
+
26
+
27
+@pytest.fixture()
28
+def mock_pve():
29
+    with responses.RequestsMock(registry=PVERegistry, assert_all_requests_are_fired=False) as rsps:
30
+        yield rsps
31
+
32
+
33
+class PVERegistry(responses.registries.FirstMatchRegistry):
34
+    base_url = "https://1.2.3.4:1234/api2/json"
35
+
36
+    common_headers = {
37
+        "Cache-Control": "max-age=0",
38
+        "Connection": "close, Keep-Alive",
39
+        "Pragma": "no-cache",
40
+        "Server": "pve-api-daemon/3.0",
41
+        "Content-Type": "application/json;charset=UTF-8",
42
+    }
43
+
44
+    def __init__(self):
45
+        super().__init__()
46
+        for resp in self._generate_static_responses():
47
+            self.add(resp)
48
+
49
+        for resp in self._generate_dynamic_responses():
50
+            self.add(resp)
51
+
52
+    def _generate_static_responses(self):
53
+        resps = []
54
+
55
+        # Basic GET requests
56
+        resps.append(
57
+            responses.Response(
58
+                method="GET",
59
+                url=self.base_url + "/version",
60
+                json={"data": {"version": "7.2-3", "release": "7.2", "repoid": "c743d6c1"}},
61
+            )
62
+        )
63
+
64
+        resps.append(
65
+            responses.Response(
66
+                method="POST",
67
+                url=re.compile(self.base_url + r"/nodes/[^/]+/storage/[^/]+/download-url"),
68
+                # "done" added to UPID so polling will terminate (status checking is tested elsewhere)
69
+                json={
70
+                    "data": "UPID:node:003094EA:095F1EFE:63E88772:download:file.iso:root@pam:done",
71
+                    "success": 1,
72
+                },
73
+            )
74
+        )
75
+
76
+        resps.append(
77
+            responses.Response(
78
+                method="POST",
79
+                url=re.compile(self.base_url + r"/nodes/[^/]+/storage/storage1/upload"),
80
+                # "done" added to UPID so polling will terminate (status checking is tested elsewhere)
81
+                json={"data": "UPID:node:0017C594:0ADB2769:63EC5455:imgcopy::root@pam:done"},
82
+            )
83
+        )
84
+        resps.append(
85
+            responses.Response(
86
+                method="POST",
87
+                url=re.compile(self.base_url + r"/nodes/[^/]+/storage/missing/upload"),
88
+                status=500,
89
+                body="storage 'missing' does not exist",
90
+            )
91
+        )
92
+
93
+        return resps
94
+
95
+    def _generate_dynamic_responses(self):
96
+        resps = []
97
+
98
+        # Authentication
99
+        resps.append(
100
+            responses.CallbackResponse(
101
+                method="POST",
102
+                url=self.base_url + "/access/ticket",
103
+                callback=self._cb_password_auth,
104
+            )
105
+        )
106
+
107
+        # Session testing
108
+        resps.append(
109
+            responses.CallbackResponse(
110
+                method="GET",
111
+                url=self.base_url + "/fake/echo",
112
+                callback=self._cb_echo,
113
+            )
114
+        )
115
+
116
+        resps.append(
117
+            responses.CallbackResponse(
118
+                method="GET",
119
+                url=re.compile(self.base_url + r"/nodes/[^/]+/qemu/[^/]+/agent/exec"),
120
+                callback=self._cb_echo,
121
+            )
122
+        )
123
+
124
+        resps.append(
125
+            responses.CallbackResponse(
126
+                method="GET",
127
+                url=re.compile(self.base_url + r"/nodes/[^/]+/qemu/[^/]+/monitor"),
128
+                callback=self._cb_qemu_monitor,
129
+            )
130
+        )
131
+
132
+        resps.append(
133
+            responses.CallbackResponse(
134
+                method="GET",
135
+                url=re.compile(self.base_url + r"/nodes/[^/]+/tasks/[^/]+/status"),
136
+                callback=self._cb_task_status,
137
+            )
138
+        )
139
+
140
+        resps.append(
141
+            responses.CallbackResponse(
142
+                method="GET",
143
+                url=re.compile(self.base_url + r"/nodes/[^/]+/query-url-metadata.*"),
144
+                callback=self._cb_url_metadata,
145
+            )
146
+        )
147
+
148
+        return resps
149
+
150
+    ###################################
151
+    # Callbacks for Dynamic Responses #
152
+    ###################################
153
+
154
+    def _cb_echo(self, request):
155
+        body = request.body
156
+        if body is not None:
157
+            if isinstance(body, MultipartEncoder):
158
+                body = body.to_string()  # really, to byte string
159
+            body = body if isinstance(body, str) else str(body, "utf-8")
160
+
161
+        resp = {
162
+            "method": request.method,
163
+            "url": request.url,
164
+            "headers": dict(request.headers),
165
+            "cookies": request._cookies.get_dict(),
166
+            "body": body,
167
+            # "body_json": dict(parse_qsl(request.body)),
168
+        }
169
+        return (200, self.common_headers, json.dumps(resp))
170
+
171
+    def _cb_password_auth(self, request):
172
+        form_data_dict = dict(parse_qsl(request.body))
173
+
174
+        # if this user should not be authenticated
175
+        if form_data_dict.get("username") == "bad_auth":
176
+            return (
177
+                401,
178
+                self.common_headers,
179
+                json.dumps({"data": None}),
180
+            )
181
+        # if this user requires OTP and it is not included
182
+        if form_data_dict.get("username") == "otp" and form_data_dict.get("otp") is None:
183
+            return (
184
+                200,
185
+                self.common_headers,
186
+                json.dumps(
187
+                    {
188
+                        "data": {
189
+                            "ticket": "otp_ticket",
190
+                            "CSRFPreventionToken": "CSRFPreventionToken",
191
+                            "NeedTFA": 1,
192
+                        }
193
+                    }
194
+                ),
195
+            )
196
+
197
+        # if this is the first ticket
198
+        if form_data_dict.get("password") != "ticket":
199
+            return (
200
+                200,
201
+                self.common_headers,
202
+                json.dumps(
203
+                    {"data": {"ticket": "ticket", "CSRFPreventionToken": "CSRFPreventionToken"}}
204
+                ),
205
+            )
206
+        # if this is refreshing the ticket, return new ticket
207
+        else:
208
+            return (
209
+                200,
210
+                self.common_headers,
211
+                json.dumps(
212
+                    {
213
+                        "data": {
214
+                            "ticket": "new_ticket",
215
+                            "CSRFPreventionToken": "CSRFPreventionToken_2",
216
+                        }
217
+                    }
218
+                ),
219
+            )
220
+
221
+    def _cb_task_status(self, request):
222
+        resp = {}
223
+        if "keep-running" in request.url:
224
+            resp = {
225
+                "data": {
226
+                    "id": "110",
227
+                    "pid": 1044989,
228
+                    "node": "node1",
229
+                    "pstart": 284768076,
230
+                    "status": "running",
231
+                    "upid": "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:keep-running",
232
+                    "starttime": 1661825068,
233
+                    "user": "root@pam",
234
+                    "type": "vzdump",
235
+                }
236
+            }
237
+
238
+        elif "stopped" in request.url:
239
+            resp = {
240
+                "data": {
241
+                    "upid": "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:stopped",
242
+                    "starttime": 1661825068,
243
+                    "user": "root@pam",
244
+                    "type": "vzdump",
245
+                    "pstart": 284768076,
246
+                    "status": "stopped",
247
+                    "exitstatus": "interrupted by signal",
248
+                    "pid": 1044989,
249
+                    "id": "110",
250
+                    "node": "node1",
251
+                }
252
+            }
253
+
254
+        elif "done" in request.url:
255
+            resp = {
256
+                "data": {
257
+                    "upid": "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:done",
258
+                    "starttime": 1661825068,
259
+                    "user": "root@pam",
260
+                    "type": "vzdump",
261
+                    "pstart": 284768076,
262
+                    "status": "stopped",
263
+                    "exitstatus": "OK",
264
+                    "pid": 1044989,
265
+                    "id": "110",
266
+                    "node": "node1",
267
+                }
268
+            }
269
+
270
+        elif "comment" in request.url:
271
+            resp = {
272
+                "data": {
273
+                    "upid": "UPID:node:00000000:00000000:00000000:task:id:root@pam:comment",
274
+                    "node": "node",
275
+                    "pid": 0,
276
+                    "pstart": 0,
277
+                    "starttime": 0,
278
+                    "type": "task",
279
+                    "id": "id",
280
+                    "user": "root@pam",
281
+                    "status": "stopped",
282
+                    "exitstatus": "OK",
283
+                }
284
+            }
285
+
286
+        return (200, self.common_headers, json.dumps(resp))
287
+
288
+    def _cb_url_metadata(self, request):
289
+        form_data_dict = dict(parse_qsl((urlparse(request.url)).query))
290
+
291
+        if "file.iso" in form_data_dict.get("url", ""):
292
+            return (
293
+                200,
294
+                self.common_headers,
295
+                json.dumps(
296
+                    {
297
+                        "data": {
298
+                            "size": 123456,
299
+                            "filename": "file.iso",
300
+                            "mimetype": "application/x-iso9660-image",
301
+                            # "mimetype": "application/octet-stream",
302
+                        },
303
+                        "success": 1,
304
+                    }
305
+                ),
306
+            )
307
+        elif "invalid.iso" in form_data_dict.get("url", ""):
308
+            return (
309
+                500,
310
+                self.common_headers,
311
+                json.dumps(
312
+                    {
313
+                        "status": 500,
314
+                        "message": "invalid server response: '500 Can't connect to sub.domain.tld:443 (certificate verify failed)'\n",
315
+                        "success": 0,
316
+                        "data": None,
317
+                    }
318
+                ),
319
+            )
320
+        elif "missing.iso" in form_data_dict.get("url", ""):
321
+            return (
322
+                500,
323
+                self.common_headers,
324
+                json.dumps(
325
+                    {
326
+                        "status": 500,
327
+                        "success": 0,
328
+                        "message": "invalid server response: '404 Not Found'\n",
329
+                        "data": None,
330
+                    }
331
+                ),
332
+            )
333
+
334
+        elif "index.html" in form_data_dict.get("url", ""):
335
+            return (
336
+                200,
337
+                self.common_headers,
338
+                json.dumps(
339
+                    {
340
+                        "success": 1,
341
+                        "data": {"filename": "index.html", "mimetype": "text/html", "size": 17664},
342
+                    }
343
+                ),
344
+            )
345
+
346
+    def _cb_qemu_monitor(self, request):
347
+        body = request.body
348
+        if body is not None:
349
+            body = body if isinstance(body, str) else str(body, "utf-8")
350
+
351
+        # if the command is an array, throw the type error PVE would throw
352
+        if "&" in body:
353
+            return (
354
+                400,
355
+                self.common_headers,
356
+                json.dumps(
357
+                    {
358
+                        "data": None,
359
+                        "errors": {"command": "type check ('string') failed - got ARRAY"},
360
+                    }
361
+                ),
362
+            )
363
+        else:
364
+            resp = {
365
+                "method": request.method,
366
+                "url": request.url,
367
+                "headers": dict(request.headers),
368
+                "cookies": request._cookies.get_dict(),
369
+                "body": body,
370
+                # "body_json": dict(parse_qsl(request.body)),
371
+            }
372
+            print(resp)
373
+            return (200, self.common_headers, json.dumps(resp))
374
diff -ruN tests/files_mock.py proxmoxer-2.2.0/tests/files_mock.py
375
--- tests/files_mock.py	1970-01-01 01:00:00.000000000 +0100
376
+++ proxmoxer-2.2.0/tests/files_mock.py	2024-12-15 02:12:42.000000000 +0000
377
@@ -0,0 +1,127 @@
378
+__author__ = "John Hollowell"
379
+__copyright__ = "(c) John Hollowell 2022"
380
+__license__ = "MIT"
381
+
382
+import re
383
+
384
+import pytest
385
+import responses
386
+from requests import exceptions
387
+
388
+from .api_mock import PVERegistry
389
+
390
+
391
+@pytest.fixture()
392
+def mock_files():
393
+    with responses.RequestsMock(
394
+        registry=FilesRegistry, assert_all_requests_are_fired=False
395
+    ) as rsps:
396
+        yield rsps
397
+
398
+
399
+class FilesRegistry(responses.registries.FirstMatchRegistry):
400
+    base_url = "https://sub.domain.tld"
401
+
402
+    common_headers = {
403
+        "Cache-Control": "max-age=0",
404
+        "Connection": "close, Keep-Alive",
405
+        "Pragma": "no-cache",
406
+        "Server": "pve-api-daemon/3.0",
407
+        "Content-Type": "application/json;charset=UTF-8",
408
+    }
409
+
410
+    def __init__(self):
411
+        super().__init__()
412
+        for resp in self._generate_static_responses():
413
+            self.add(resp)
414
+
415
+    def _generate_static_responses(self):
416
+        resps = []
417
+
418
+        # Basic GET requests
419
+        resps.append(responses.Response(method="GET", url=self.base_url, body="hello world"))
420
+        resps.append(
421
+            responses.Response(method="GET", url=self.base_url + "/file.iso", body="CONTENTS")
422
+        )
423
+
424
+        # sibling
425
+        resps.append(
426
+            responses.Response(
427
+                method="GET", url=self.base_url + "/sibling/file.iso", body="CONTENTS\n"
428
+            )
429
+        )
430
+        resps.append(
431
+            responses.Response(
432
+                method="GET",
433
+                url=self.base_url + "/sibling/TESTINGSUMS",
434
+                body="this_is_the_hash  file.iso",
435
+            )
436
+        )
437
+
438
+        # extension
439
+        resps.append(
440
+            responses.Response(
441
+                method="GET", url=self.base_url + "/extension/file.iso", body="CONTENTS\n"
442
+            )
443
+        )
444
+        resps.append(
445
+            responses.Response(
446
+                method="GET",
447
+                url=self.base_url + "/extension/file.iso.testing",
448
+                body="this_is_the_hash  file.iso",
449
+            )
450
+        )
451
+        resps.append(
452
+            responses.Response(
453
+                method="GET",
454
+                url=self.base_url + "/extension/connectionerror.iso.testing",
455
+                body=exceptions.ConnectionError(),
456
+            )
457
+        )
458
+        resps.append(
459
+            responses.Response(
460
+                method="GET",
461
+                url=self.base_url + "/extension/readtimeout.iso.testing",
462
+                body=exceptions.ReadTimeout(),
463
+            )
464
+        )
465
+
466
+        # extension upper
467
+        resps.append(
468
+            responses.Response(
469
+                method="GET", url=self.base_url + "/upper/file.iso", body="CONTENTS\n"
470
+            )
471
+        )
472
+        resps.append(
473
+            responses.Response(
474
+                method="GET",
475
+                url=self.base_url + "/upper/file.iso.TESTING",
476
+                body="this_is_the_hash  file.iso",
477
+            )
478
+        )
479
+
480
+        resps.append(
481
+            responses.Response(
482
+                method="GET",
483
+                url=re.compile(self.base_url + r"/checksums/file.iso.\w+"),
484
+                body="1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 file.iso",
485
+            )
486
+        )
487
+
488
+        return resps
489
+
490
+
491
+@pytest.fixture()
492
+def mock_files_and_pve():
493
+    with responses.RequestsMock(registry=BothRegistry, assert_all_requests_are_fired=False) as rsps:
494
+        yield rsps
495
+
496
+
497
+class BothRegistry(responses.registries.FirstMatchRegistry):
498
+    def __init__(self):
499
+        super().__init__()
500
+        registries = [FilesRegistry(), PVERegistry()]
501
+
502
+        for reg in registries:
503
+            for resp in reg.registered:
504
+                self.add(resp)
505
diff -ruN tests/known_issues.json proxmoxer-2.2.0/tests/known_issues.json
506
--- tests/known_issues.json	1970-01-01 01:00:00.000000000 +0100
507
+++ proxmoxer-2.2.0/tests/known_issues.json	2024-12-15 02:12:42.000000000 +0000
508
@@ -0,0 +1,520 @@
509
+{
510
+  "errors": [],
511
+  "generated_at": "2022-08-25T03:08:48Z",
512
+  "metrics": {
513
+    "_totals": {
514
+      "CONFIDENCE.HIGH": 3,
515
+      "CONFIDENCE.LOW": 0,
516
+      "CONFIDENCE.MEDIUM": 11,
517
+      "CONFIDENCE.UNDEFINED": 0,
518
+      "SEVERITY.HIGH": 0,
519
+      "SEVERITY.LOW": 3,
520
+      "SEVERITY.MEDIUM": 11,
521
+      "SEVERITY.UNDEFINED": 0,
522
+      "loc": 1947,
523
+      "nosec": 0,
524
+      "skipped_tests": 0
525
+    },
526
+    "proxmoxer/__init__.py": {
527
+      "CONFIDENCE.HIGH": 0,
528
+      "CONFIDENCE.LOW": 0,
529
+      "CONFIDENCE.MEDIUM": 0,
530
+      "CONFIDENCE.UNDEFINED": 0,
531
+      "SEVERITY.HIGH": 0,
532
+      "SEVERITY.LOW": 0,
533
+      "SEVERITY.MEDIUM": 0,
534
+      "SEVERITY.UNDEFINED": 0,
535
+      "loc": 5,
536
+      "nosec": 0,
537
+      "skipped_tests": 0
538
+    },
539
+    "proxmoxer/backends/__init__.py": {
540
+      "CONFIDENCE.HIGH": 0,
541
+      "CONFIDENCE.LOW": 0,
542
+      "CONFIDENCE.MEDIUM": 0,
543
+      "CONFIDENCE.UNDEFINED": 0,
544
+      "SEVERITY.HIGH": 0,
545
+      "SEVERITY.LOW": 0,
546
+      "SEVERITY.MEDIUM": 0,
547
+      "SEVERITY.UNDEFINED": 0,
548
+      "loc": 3,
549
+      "nosec": 0,
550
+      "skipped_tests": 0
551
+    },
552
+    "proxmoxer/backends/command_base.py": {
553
+      "CONFIDENCE.HIGH": 0,
554
+      "CONFIDENCE.LOW": 0,
555
+      "CONFIDENCE.MEDIUM": 0,
556
+      "CONFIDENCE.UNDEFINED": 0,
557
+      "SEVERITY.HIGH": 0,
558
+      "SEVERITY.LOW": 0,
559
+      "SEVERITY.MEDIUM": 0,
560
+      "SEVERITY.UNDEFINED": 0,
561
+      "loc": 115,
562
+      "nosec": 0,
563
+      "skipped_tests": 0
564
+    },
565
+    "proxmoxer/backends/https.py": {
566
+      "CONFIDENCE.HIGH": 1,
567
+      "CONFIDENCE.LOW": 0,
568
+      "CONFIDENCE.MEDIUM": 0,
569
+      "CONFIDENCE.UNDEFINED": 0,
570
+      "SEVERITY.HIGH": 0,
571
+      "SEVERITY.LOW": 1,
572
+      "SEVERITY.MEDIUM": 0,
573
+      "SEVERITY.UNDEFINED": 0,
574
+      "loc": 286,
575
+      "nosec": 0,
576
+      "skipped_tests": 0
577
+    },
578
+    "proxmoxer/backends/local.py": {
579
+      "CONFIDENCE.HIGH": 2,
580
+      "CONFIDENCE.LOW": 0,
581
+      "CONFIDENCE.MEDIUM": 0,
582
+      "CONFIDENCE.UNDEFINED": 0,
583
+      "SEVERITY.HIGH": 0,
584
+      "SEVERITY.LOW": 2,
585
+      "SEVERITY.MEDIUM": 0,
586
+      "SEVERITY.UNDEFINED": 0,
587
+      "loc": 14,
588
+      "nosec": 0,
589
+      "skipped_tests": 0
590
+    },
591
+    "proxmoxer/backends/openssh.py": {
592
+      "CONFIDENCE.HIGH": 0,
593
+      "CONFIDENCE.LOW": 0,
594
+      "CONFIDENCE.MEDIUM": 0,
595
+      "CONFIDENCE.UNDEFINED": 0,
596
+      "SEVERITY.HIGH": 0,
597
+      "SEVERITY.LOW": 0,
598
+      "SEVERITY.MEDIUM": 0,
599
+      "SEVERITY.UNDEFINED": 0,
600
+      "loc": 53,
601
+      "nosec": 0,
602
+      "skipped_tests": 0
603
+    },
604
+    "proxmoxer/backends/ssh_paramiko.py": {
605
+      "CONFIDENCE.HIGH": 0,
606
+      "CONFIDENCE.LOW": 0,
607
+      "CONFIDENCE.MEDIUM": 1,
608
+      "CONFIDENCE.UNDEFINED": 0,
609
+      "SEVERITY.HIGH": 0,
610
+      "SEVERITY.LOW": 0,
611
+      "SEVERITY.MEDIUM": 1,
612
+      "SEVERITY.UNDEFINED": 0,
613
+      "loc": 58,
614
+      "nosec": 0,
615
+      "skipped_tests": 0
616
+    },
617
+    "proxmoxer/core.py": {
618
+      "CONFIDENCE.HIGH": 0,
619
+      "CONFIDENCE.LOW": 0,
620
+      "CONFIDENCE.MEDIUM": 0,
621
+      "CONFIDENCE.UNDEFINED": 0,
622
+      "SEVERITY.HIGH": 0,
623
+      "SEVERITY.LOW": 0,
624
+      "SEVERITY.MEDIUM": 0,
625
+      "SEVERITY.UNDEFINED": 0,
626
+      "loc": 155,
627
+      "nosec": 0,
628
+      "skipped_tests": 0
629
+    },
630
+    "tests/api_mock.py": {
631
+      "CONFIDENCE.HIGH": 0,
632
+      "CONFIDENCE.LOW": 0,
633
+      "CONFIDENCE.MEDIUM": 0,
634
+      "CONFIDENCE.UNDEFINED": 0,
635
+      "SEVERITY.HIGH": 0,
636
+      "SEVERITY.LOW": 0,
637
+      "SEVERITY.MEDIUM": 0,
638
+      "SEVERITY.UNDEFINED": 0,
639
+      "loc": 108,
640
+      "nosec": 0,
641
+      "skipped_tests": 0
642
+    },
643
+    "tests/test_command_base.py": {
644
+      "CONFIDENCE.HIGH": 0,
645
+      "CONFIDENCE.LOW": 0,
646
+      "CONFIDENCE.MEDIUM": 2,
647
+      "CONFIDENCE.UNDEFINED": 0,
648
+      "SEVERITY.HIGH": 0,
649
+      "SEVERITY.LOW": 0,
650
+      "SEVERITY.MEDIUM": 2,
651
+      "SEVERITY.UNDEFINED": 0,
652
+      "loc": 195,
653
+      "nosec": 0,
654
+      "skipped_tests": 0
655
+    },
656
+    "tests/test_core.py": {
657
+      "CONFIDENCE.HIGH": 0,
658
+      "CONFIDENCE.LOW": 0,
659
+      "CONFIDENCE.MEDIUM": 0,
660
+      "CONFIDENCE.UNDEFINED": 0,
661
+      "SEVERITY.HIGH": 0,
662
+      "SEVERITY.LOW": 0,
663
+      "SEVERITY.MEDIUM": 0,
664
+      "SEVERITY.UNDEFINED": 0,
665
+      "loc": 241,
666
+      "nosec": 0,
667
+      "skipped_tests": 0
668
+    },
669
+    "tests/test_https.py": {
670
+      "CONFIDENCE.HIGH": 0,
671
+      "CONFIDENCE.LOW": 0,
672
+      "CONFIDENCE.MEDIUM": 0,
673
+      "CONFIDENCE.UNDEFINED": 0,
674
+      "SEVERITY.HIGH": 0,
675
+      "SEVERITY.LOW": 0,
676
+      "SEVERITY.MEDIUM": 0,
677
+      "SEVERITY.UNDEFINED": 0,
678
+      "loc": 362,
679
+      "nosec": 0,
680
+      "skipped_tests": 0
681
+    },
682
+    "tests/test_https_helpers.py": {
683
+      "CONFIDENCE.HIGH": 0,
684
+      "CONFIDENCE.LOW": 0,
685
+      "CONFIDENCE.MEDIUM": 0,
686
+      "CONFIDENCE.UNDEFINED": 0,
687
+      "SEVERITY.HIGH": 0,
688
+      "SEVERITY.LOW": 0,
689
+      "SEVERITY.MEDIUM": 0,
690
+      "SEVERITY.UNDEFINED": 0,
691
+      "loc": 62,
692
+      "nosec": 0,
693
+      "skipped_tests": 0
694
+    },
695
+    "tests/test_imports.py": {
696
+      "CONFIDENCE.HIGH": 0,
697
+      "CONFIDENCE.LOW": 0,
698
+      "CONFIDENCE.MEDIUM": 0,
699
+      "CONFIDENCE.UNDEFINED": 0,
700
+      "SEVERITY.HIGH": 0,
701
+      "SEVERITY.LOW": 0,
702
+      "SEVERITY.MEDIUM": 0,
703
+      "SEVERITY.UNDEFINED": 0,
704
+      "loc": 80,
705
+      "nosec": 0,
706
+      "skipped_tests": 0
707
+    },
708
+    "tests/test_local.py": {
709
+      "CONFIDENCE.HIGH": 0,
710
+      "CONFIDENCE.LOW": 0,
711
+      "CONFIDENCE.MEDIUM": 0,
712
+      "CONFIDENCE.UNDEFINED": 0,
713
+      "SEVERITY.HIGH": 0,
714
+      "SEVERITY.LOW": 0,
715
+      "SEVERITY.MEDIUM": 0,
716
+      "SEVERITY.UNDEFINED": 0,
717
+      "loc": 35,
718
+      "nosec": 0,
719
+      "skipped_tests": 0
720
+    },
721
+    "tests/test_openssh.py": {
722
+      "CONFIDENCE.HIGH": 0,
723
+      "CONFIDENCE.LOW": 0,
724
+      "CONFIDENCE.MEDIUM": 2,
725
+      "CONFIDENCE.UNDEFINED": 0,
726
+      "SEVERITY.HIGH": 0,
727
+      "SEVERITY.LOW": 0,
728
+      "SEVERITY.MEDIUM": 2,
729
+      "SEVERITY.UNDEFINED": 0,
730
+      "loc": 62,
731
+      "nosec": 0,
732
+      "skipped_tests": 0
733
+    },
734
+    "tests/test_paramiko.py": {
735
+      "CONFIDENCE.HIGH": 0,
736
+      "CONFIDENCE.LOW": 0,
737
+      "CONFIDENCE.MEDIUM": 6,
738
+      "CONFIDENCE.UNDEFINED": 0,
739
+      "SEVERITY.HIGH": 0,
740
+      "SEVERITY.LOW": 0,
741
+      "SEVERITY.MEDIUM": 6,
742
+      "SEVERITY.UNDEFINED": 0,
743
+      "loc": 113,
744
+      "nosec": 0,
745
+      "skipped_tests": 0
746
+    }
747
+  },
748
+  "results": [
749
+    {
750
+      "code": "332     def get_serializer(self):\n333         assert self.mode == \"json\"\n334         return JsonSerializer()\n",
751
+      "col_offset": 8,
752
+      "filename": "proxmoxer/backends/https.py",
753
+      "issue_confidence": "HIGH",
754
+      "issue_cwe": {
755
+        "id": 703,
756
+        "link": "https://cwe.mitre.org/data/definitions/703.html"
757
+      },
758
+      "issue_severity": "LOW",
759
+      "issue_text": "Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.",
760
+      "line_number": 333,
761
+      "line_range": [
762
+        333
763
+      ],
764
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b101_assert_used.html",
765
+      "test_id": "B101",
766
+      "test_name": "assert_used"
767
+    },
768
+    {
769
+      "code": "1 import shutil\n2 from subprocess import PIPE, Popen\n3 \n4 from proxmoxer.backends.command_base import CommandBaseBackend, CommandBaseSession\n",
770
+      "col_offset": 0,
771
+      "filename": "proxmoxer/backends/local.py",
772
+      "issue_confidence": "HIGH",
773
+      "issue_cwe": {
774
+        "id": 78,
775
+        "link": "https://cwe.mitre.org/data/definitions/78.html"
776
+      },
777
+      "issue_severity": "LOW",
778
+      "issue_text": "Consider possible security implications associated with the subprocess module.",
779
+      "line_number": 2,
780
+      "line_range": [
781
+        2,
782
+        3
783
+      ],
784
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/blacklists/blacklist_imports.html#b404-import-subprocess",
785
+      "test_id": "B404",
786
+      "test_name": "blacklist"
787
+    },
788
+    {
789
+      "code": "8     def _exec(self, cmd):\n9         proc = Popen(cmd, stdout=PIPE, stderr=PIPE)\n10         stdout, stderr = proc.communicate(timeout=self.timeout)\n",
790
+      "col_offset": 15,
791
+      "filename": "proxmoxer/backends/local.py",
792
+      "issue_confidence": "HIGH",
793
+      "issue_cwe": {
794
+        "id": 78,
795
+        "link": "https://cwe.mitre.org/data/definitions/78.html"
796
+      },
797
+      "issue_severity": "LOW",
798
+      "issue_text": "subprocess call - check for execution of untrusted input.",
799
+      "line_number": 9,
800
+      "line_range": [
801
+        9
802
+      ],
803
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b603_subprocess_without_shell_equals_true.html",
804
+      "test_id": "B603",
805
+      "test_name": "subprocess_without_shell_equals_true"
806
+    },
807
+    {
808
+      "code": "62         session = self.ssh_client.get_transport().open_session()\n63         session.exec_command(shell_join(cmd))\n64         stdout = session.makefile(\"rb\", -1).read().decode()\n",
809
+      "col_offset": 8,
810
+      "filename": "proxmoxer/backends/ssh_paramiko.py",
811
+      "issue_confidence": "MEDIUM",
812
+      "issue_cwe": {
813
+        "id": 78,
814
+        "link": "https://cwe.mitre.org/data/definitions/78.html"
815
+      },
816
+      "issue_severity": "MEDIUM",
817
+      "issue_text": "Possible shell injection via Paramiko call, check inputs are properly sanitized.",
818
+      "line_number": 63,
819
+      "line_range": [
820
+        63
821
+      ],
822
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b601_paramiko_calls.html",
823
+      "test_id": "B601",
824
+      "test_name": "paramiko_calls"
825
+    },
826
+    {
827
+      "code": "39         with pytest.raises(NotImplementedError), tempfile.TemporaryFile(\"w+b\") as f_obj:\n40             self._session.upload_file_obj(f_obj, \"/tmp/file.iso\")\n41 \n",
828
+      "col_offset": 49,
829
+      "filename": "tests/test_command_base.py",
830
+      "issue_confidence": "MEDIUM",
831
+      "issue_cwe": {
832
+        "id": 377,
833
+        "link": "https://cwe.mitre.org/data/definitions/377.html"
834
+      },
835
+      "issue_severity": "MEDIUM",
836
+      "issue_text": "Probable insecure usage of temp file/directory.",
837
+      "line_number": 40,
838
+      "line_range": [
839
+        40
840
+      ],
841
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b108_hardcoded_tmp_directory.html",
842
+      "test_id": "B108",
843
+      "test_name": "hardcoded_tmp_directory"
844
+    },
845
+    {
846
+      "code": "160                 \"-tmpfilename\",\n161                 \"/tmp/tmpasdfasdf\",\n162                 \"--output-format\",\n163                 \"json\",\n164             ]\n165 \n166 \n167 class TestJsonSimpleSerializer:\n168     _serializer = command_base.JsonSimpleSerializer()\n169 \n170     def test_loads_pass(self):\n171         input_str = '{\"key1\": \"value1\", \"key2\": \"value2\"}'\n172         exp_output = {\"key1\": \"value1\", \"key2\": \"value2\"}\n173 \n",
847
+      "col_offset": 16,
848
+      "filename": "tests/test_command_base.py",
849
+      "issue_confidence": "MEDIUM",
850
+      "issue_cwe": {
851
+        "id": 377,
852
+        "link": "https://cwe.mitre.org/data/definitions/377.html"
853
+      },
854
+      "issue_severity": "MEDIUM",
855
+      "issue_text": "Probable insecure usage of temp file/directory.",
856
+      "line_number": 161,
857
+      "line_range": [
858
+        152,
859
+        153,
860
+        154,
861
+        155,
862
+        156,
863
+        157,
864
+        158,
865
+        159,
866
+        160,
867
+        161,
868
+        162,
869
+        163
870
+      ],
871
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b108_hardcoded_tmp_directory.html",
872
+      "test_id": "B108",
873
+      "test_name": "hardcoded_tmp_directory"
874
+    },
875
+    {
876
+      "code": "61         with tempfile.NamedTemporaryFile(\"r\") as f_obj:\n62             mock_session.upload_file_obj(f_obj, \"/tmp/file\")\n63 \n",
877
+      "col_offset": 48,
878
+      "filename": "tests/test_openssh.py",
879
+      "issue_confidence": "MEDIUM",
880
+      "issue_cwe": {
881
+        "id": 377,
882
+        "link": "https://cwe.mitre.org/data/definitions/377.html"
883
+      },
884
+      "issue_severity": "MEDIUM",
885
+      "issue_text": "Probable insecure usage of temp file/directory.",
886
+      "line_number": 62,
887
+      "line_range": [
888
+        62
889
+      ],
890
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b108_hardcoded_tmp_directory.html",
891
+      "test_id": "B108",
892
+      "test_name": "hardcoded_tmp_directory"
893
+    },
894
+    {
895
+      "code": "65                 (f_obj,),\n66                 target=\"/tmp/file\",\n67             )\n",
896
+      "col_offset": 23,
897
+      "filename": "tests/test_openssh.py",
898
+      "issue_confidence": "MEDIUM",
899
+      "issue_cwe": {
900
+        "id": 377,
901
+        "link": "https://cwe.mitre.org/data/definitions/377.html"
902
+      },
903
+      "issue_severity": "MEDIUM",
904
+      "issue_text": "Probable insecure usage of temp file/directory.",
905
+      "line_number": 66,
906
+      "line_range": [
907
+        66
908
+      ],
909
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b108_hardcoded_tmp_directory.html",
910
+      "test_id": "B108",
911
+      "test_name": "hardcoded_tmp_directory"
912
+    },
913
+    {
914
+      "code": "23         sess = ssh_paramiko.SshParamikoSession(\n24             \"host\", \"user\", password=\"password\", private_key_file=\"/tmp/key_file\", port=1234\n25         )\n",
915
+      "col_offset": 66,
916
+      "filename": "tests/test_paramiko.py",
917
+      "issue_confidence": "MEDIUM",
918
+      "issue_cwe": {
919
+        "id": 377,
920
+        "link": "https://cwe.mitre.org/data/definitions/377.html"
921
+      },
922
+      "issue_severity": "MEDIUM",
923
+      "issue_text": "Probable insecure usage of temp file/directory.",
924
+      "line_number": 24,
925
+      "line_range": [
926
+        24
927
+      ],
928
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b108_hardcoded_tmp_directory.html",
929
+      "test_id": "B108",
930
+      "test_name": "hardcoded_tmp_directory"
931
+    },
932
+    {
933
+      "code": "29         assert sess.password == \"password\"\n30         assert sess.private_key_file == \"/tmp/key_file\"\n31         assert sess.port == 1234\n",
934
+      "col_offset": 40,
935
+      "filename": "tests/test_paramiko.py",
936
+      "issue_confidence": "MEDIUM",
937
+      "issue_cwe": {
938
+        "id": 377,
939
+        "link": "https://cwe.mitre.org/data/definitions/377.html"
940
+      },
941
+      "issue_severity": "MEDIUM",
942
+      "issue_text": "Probable insecure usage of temp file/directory.",
943
+      "line_number": 30,
944
+      "line_range": [
945
+        30
946
+      ],
947
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b108_hardcoded_tmp_directory.html",
948
+      "test_id": "B108",
949
+      "test_name": "hardcoded_tmp_directory"
950
+    },
951
+    {
952
+      "code": "55         sess = ssh_paramiko.SshParamikoSession(\n56             \"host\", \"user\", password=\"password\", private_key_file=\"/tmp/key_file\", port=1234\n57         )\n",
953
+      "col_offset": 66,
954
+      "filename": "tests/test_paramiko.py",
955
+      "issue_confidence": "MEDIUM",
956
+      "issue_cwe": {
957
+        "id": 377,
958
+        "link": "https://cwe.mitre.org/data/definitions/377.html"
959
+      },
960
+      "issue_severity": "MEDIUM",
961
+      "issue_text": "Probable insecure usage of temp file/directory.",
962
+      "line_number": 56,
963
+      "line_range": [
964
+        56
965
+      ],
966
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b108_hardcoded_tmp_directory.html",
967
+      "test_id": "B108",
968
+      "test_name": "hardcoded_tmp_directory"
969
+    },
970
+    {
971
+      "code": "63             look_for_keys=True,\n64             key_filename=\"/tmp/key_file\",\n65             password=\"password\",\n",
972
+      "col_offset": 25,
973
+      "filename": "tests/test_paramiko.py",
974
+      "issue_confidence": "MEDIUM",
975
+      "issue_cwe": {
976
+        "id": 377,
977
+        "link": "https://cwe.mitre.org/data/definitions/377.html"
978
+      },
979
+      "issue_severity": "MEDIUM",
980
+      "issue_text": "Probable insecure usage of temp file/directory.",
981
+      "line_number": 64,
982
+      "line_range": [
983
+        64
984
+      ],
985
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b108_hardcoded_tmp_directory.html",
986
+      "test_id": "B108",
987
+      "test_name": "hardcoded_tmp_directory"
988
+    },
989
+    {
990
+      "code": "110         with tempfile.NamedTemporaryFile(\"r\") as f_obj:\n111             sess.upload_file_obj(f_obj, \"/tmp/file\")\n112 \n",
991
+      "col_offset": 40,
992
+      "filename": "tests/test_paramiko.py",
993
+      "issue_confidence": "MEDIUM",
994
+      "issue_cwe": {
995
+        "id": 377,
996
+        "link": "https://cwe.mitre.org/data/definitions/377.html"
997
+      },
998
+      "issue_severity": "MEDIUM",
999
+      "issue_text": "Probable insecure usage of temp file/directory.",
1000
+      "line_number": 111,
1001
+      "line_range": [
1002
+        111
1003
+      ],
1004
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b108_hardcoded_tmp_directory.html",
1005
+      "test_id": "B108",
1006
+      "test_name": "hardcoded_tmp_directory"
1007
+    },
1008
+    {
1009
+      "code": "112 \n113             mock_sftp.putfo.assert_called_once_with(f_obj, \"/tmp/file\")\n114 \n",
1010
+      "col_offset": 59,
1011
+      "filename": "tests/test_paramiko.py",
1012
+      "issue_confidence": "MEDIUM",
1013
+      "issue_cwe": {
1014
+        "id": 377,
1015
+        "link": "https://cwe.mitre.org/data/definitions/377.html"
1016
+      },
1017
+      "issue_severity": "MEDIUM",
1018
+      "issue_text": "Probable insecure usage of temp file/directory.",
1019
+      "line_number": 113,
1020
+      "line_range": [
1021
+        113
1022
+      ],
1023
+      "more_info": "https://bandit.readthedocs.io/en/1.7.4/plugins/b108_hardcoded_tmp_directory.html",
1024
+      "test_id": "B108",
1025
+      "test_name": "hardcoded_tmp_directory"
1026
+    }
1027
+  ]
1028
+}
1029
\ No newline at end of file
1030
diff -ruN tests/tools/__init__.py proxmoxer-2.2.0/tests/tools/__init__.py
1031
--- tests/tools/__init__.py	1970-01-01 01:00:00.000000000 +0100
1032
+++ proxmoxer-2.2.0/tests/tools/__init__.py	2024-12-15 02:12:42.000000000 +0000
1033
@@ -0,0 +1,3 @@
1034
+__author__ = "John Hollowell"
1035
+__copyright__ = "(c) John Hollowell 2022"
1036
+__license__ = "MIT"
1037
diff -ruN tests/tools/test_files.py proxmoxer-2.2.0/tests/tools/test_files.py
1038
--- tests/tools/test_files.py	1970-01-01 01:00:00.000000000 +0100
1039
+++ proxmoxer-2.2.0/tests/tools/test_files.py	2024-12-15 02:12:42.000000000 +0000
1040
@@ -0,0 +1,375 @@
1041
+__author__ = "John Hollowell"
1042
+__copyright__ = "(c) John Hollowell 2023"
1043
+__license__ = "MIT"
1044
+
1045
+import logging
1046
+import tempfile
1047
+from unittest import mock
1048
+
1049
+import pytest
1050
+
1051
+from proxmoxer import ProxmoxAPI, core
1052
+from proxmoxer.tools import ChecksumInfo, Files, SupportedChecksums
1053
+
1054
+from ..api_mock import mock_pve  # pylint: disable=unused-import # noqa: F401
1055
+from ..files_mock import (  # pylint: disable=unused-import # noqa: F401
1056
+    mock_files,
1057
+    mock_files_and_pve,
1058
+)
1059
+
1060
+MODULE_LOGGER_NAME = "proxmoxer.tools.files"
1061
+
1062
+
1063
+class TestChecksumInfo:
1064
+    def test_basic(self):
1065
+        info = ChecksumInfo("name", 123)
1066
+
1067
+        assert info.name == "name"
1068
+        assert info.hex_size == 123
1069
+
1070
+    def test_str(self):
1071
+        info = ChecksumInfo("name", 123)
1072
+
1073
+        assert str(info) == "name"
1074
+
1075
+    def test_repr(self):
1076
+        info = ChecksumInfo("name", 123)
1077
+
1078
+        assert repr(info) == "name (123 digits)"
1079
+
1080
+
1081
+class TestGetChecksum:
1082
+    def test_get_checksum_from_sibling_file_success(self, mock_files):
1083
+        url = "https://sub.domain.tld/sibling/file.iso"
1084
+        exp_hash = "this_is_the_hash"
1085
+        info = ChecksumInfo("testing", 16)
1086
+        res1 = Files._get_checksum_from_sibling_file(url, checksum_info=info)
1087
+        res2 = Files._get_checksum_from_sibling_file(url, checksum_info=info, filename="file.iso")
1088
+
1089
+        assert res1 == exp_hash
1090
+        assert res2 == exp_hash
1091
+
1092
+    def test_get_checksum_from_sibling_file_fail(self, mock_files):
1093
+        url = "https://sub.domain.tld/sibling/missing.iso"
1094
+        info = ChecksumInfo("testing", 16)
1095
+        res1 = Files._get_checksum_from_sibling_file(url, checksum_info=info)
1096
+        res2 = Files._get_checksum_from_sibling_file(
1097
+            url, checksum_info=info, filename="missing.iso"
1098
+        )
1099
+
1100
+        assert res1 is None
1101
+        assert res2 is None
1102
+
1103
+    def test_get_checksum_from_extension_success(self, mock_files):
1104
+        url = "https://sub.domain.tld/extension/file.iso"
1105
+        exp_hash = "this_is_the_hash"
1106
+        info = ChecksumInfo("testing", 16)
1107
+        res1 = Files._get_checksum_from_extension(url, checksum_info=info)
1108
+        res2 = Files._get_checksum_from_extension(url, checksum_info=info, filename="file.iso")
1109
+
1110
+        assert res1 == exp_hash
1111
+        assert res2 == exp_hash
1112
+
1113
+    def test_get_checksum_from_extension_fail(self, mock_files):
1114
+        url = "https://sub.domain.tld/extension/missing.iso"
1115
+
1116
+        info = ChecksumInfo("testing", 16)
1117
+        res1 = Files._get_checksum_from_extension(url, checksum_info=info)
1118
+        res2 = Files._get_checksum_from_extension(
1119
+            url, checksum_info=info, filename="connectionerror.iso"
1120
+        )
1121
+        res3 = Files._get_checksum_from_extension(
1122
+            url, checksum_info=info, filename="readtimeout.iso"
1123
+        )
1124
+
1125
+        assert res1 is None
1126
+        assert res2 is None
1127
+        assert res3 is None
1128
+
1129
+    def test_get_checksum_from_extension_upper_success(self, mock_files):
1130
+        url = "https://sub.domain.tld/upper/file.iso"
1131
+        exp_hash = "this_is_the_hash"
1132
+        info = ChecksumInfo("testing", 16)
1133
+        res1 = Files._get_checksum_from_extension_upper(url, checksum_info=info)
1134
+        res2 = Files._get_checksum_from_extension_upper(
1135
+            url, checksum_info=info, filename="file.iso"
1136
+        )
1137
+
1138
+        assert res1 == exp_hash
1139
+        assert res2 == exp_hash
1140
+
1141
+    def test_get_checksum_from_extension_upper_fail(self, mock_files):
1142
+        url = "https://sub.domain.tld/upper/missing.iso"
1143
+        info = ChecksumInfo("testing", 16)
1144
+        res1 = Files._get_checksum_from_extension_upper(url, checksum_info=info)
1145
+        res2 = Files._get_checksum_from_extension_upper(
1146
+            url, checksum_info=info, filename="missing.iso"
1147
+        )
1148
+
1149
+        assert res1 is None
1150
+        assert res2 is None
1151
+
1152
+    def test_get_checksums_from_file_url_all_checksums(self, mock_files):
1153
+        base_url = "https://sub.domain.tld/checksums/file.iso"
1154
+        full_checksum_string = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
1155
+        for types_enum in SupportedChecksums:
1156
+            checksum_info = types_enum.value
1157
+
1158
+            data = Files.get_checksums_from_file_url(base_url, preferred_type=checksum_info)
1159
+
1160
+            assert data[0] == full_checksum_string[0 : checksum_info.hex_size]
1161
+            assert data[1] == checksum_info
1162
+
1163
+    def test_get_checksums_from_file_url_missing(self, mock_files):
1164
+        url = "https://sub.domain.tld/missing.iso"
1165
+
1166
+        data = Files.get_checksums_from_file_url(url)
1167
+
1168
+        assert data[0] is None
1169
+        assert data[1] is None
1170
+
1171
+
1172
+class TestFiles:
1173
+    prox = ProxmoxAPI("1.2.3.4:1234", token_name="name", token_value="value")
1174
+
1175
+    def test_init_basic(self):
1176
+        f = Files(self.prox, "node1", "storage1")
1177
+
1178
+        assert f._prox == self.prox
1179
+        assert f._node == "node1"
1180
+        assert f._storage == "storage1"
1181
+
1182
+    def test_repr(self):
1183
+        f = Files(self.prox, "node1", "storage1")
1184
+        assert (
1185
+            repr(f)
1186
+            == "Files (node1/storage1 at ProxmoxAPI (https backend for https://1.2.3.4:1234/api2/json))"
1187
+        )
1188
+
1189
+    def test_get_file_info_pass(self, mock_pve):
1190
+        f = Files(self.prox, "node1", "storage1")
1191
+        info = f.get_file_info("https://sub.domain.tld/file.iso")
1192
+
1193
+        assert info["filename"] == "file.iso"
1194
+        assert info["mimetype"] == "application/x-iso9660-image"
1195
+        assert info["size"] == 123456
1196
+
1197
+    def test_get_file_info_fail(self, mock_pve):
1198
+        f = Files(self.prox, "node1", "storage1")
1199
+        info = f.get_file_info("https://sub.domain.tld/invalid.iso")
1200
+
1201
+        assert info is None
1202
+
1203
+
1204
+class TestFilesDownload:
1205
+    prox = ProxmoxAPI("1.2.3.4:1234", token_name="name", token_value="value")
1206
+    f = Files(prox, "node1", "storage1")
1207
+
1208
+    def test_download_discover_checksum(self, mock_files_and_pve, caplog):
1209
+        status = self.f.download_file_to_storage("https://sub.domain.tld/checksums/file.iso")
1210
+
1211
+        # this is the default "done" task mock information
1212
+        assert status == {
1213
+            "upid": "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:done",
1214
+            "starttime": 1661825068,
1215
+            "user": "root@pam",
1216
+            "type": "vzdump",
1217
+            "pstart": 284768076,
1218
+            "status": "stopped",
1219
+            "exitstatus": "OK",
1220
+            "pid": 1044989,
1221
+            "id": "110",
1222
+            "node": "node1",
1223
+        }
1224
+        assert caplog.record_tuples == []
1225
+
1226
+    def test_download_no_blocking(self, mock_files_and_pve, caplog):
1227
+        status = self.f.download_file_to_storage(
1228
+            "https://sub.domain.tld/checksums/file.iso", blocking_status=False
1229
+        )
1230
+
1231
+        # this is the default "done" task mock information
1232
+        assert status == {
1233
+            "upid": "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:done",
1234
+            "starttime": 1661825068,
1235
+            "user": "root@pam",
1236
+            "type": "vzdump",
1237
+            "pstart": 284768076,
1238
+            "status": "stopped",
1239
+            "exitstatus": "OK",
1240
+            "pid": 1044989,
1241
+            "id": "110",
1242
+            "node": "node1",
1243
+        }
1244
+        assert caplog.record_tuples == []
1245
+
1246
+    def test_download_no_discover_checksum(self, mock_files_and_pve, caplog):
1247
+        caplog.set_level(logging.WARNING, logger=MODULE_LOGGER_NAME)
1248
+
1249
+        status = self.f.download_file_to_storage("https://sub.domain.tld/file.iso")
1250
+
1251
+        # this is the default "stopped" task mock information
1252
+        assert status == {
1253
+            "upid": "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:done",
1254
+            "starttime": 1661825068,
1255
+            "user": "root@pam",
1256
+            "type": "vzdump",
1257
+            "pstart": 284768076,
1258
+            "status": "stopped",
1259
+            "exitstatus": "OK",
1260
+            "pid": 1044989,
1261
+            "id": "110",
1262
+            "node": "node1",
1263
+        }
1264
+        assert caplog.record_tuples == [
1265
+            (
1266
+                MODULE_LOGGER_NAME,
1267
+                logging.WARNING,
1268
+                "Unable to discover checksum. Will not do checksum validation",
1269
+            ),
1270
+        ]
1271
+
1272
+    def test_uneven_checksum(self, caplog, mock_files_and_pve):
1273
+        caplog.set_level(logging.DEBUG, logger=MODULE_LOGGER_NAME)
1274
+        status = self.f.download_file_to_storage("https://sub.domain.tld/file.iso", checksum="asdf")
1275
+
1276
+        assert status is None
1277
+
1278
+        assert caplog.record_tuples == [
1279
+            (
1280
+                MODULE_LOGGER_NAME,
1281
+                logging.ERROR,
1282
+                "Must pass both checksum and checksum_type or leave both None for auto-discovery",
1283
+            ),
1284
+        ]
1285
+
1286
+    def test_uneven_checksum_type(self, caplog, mock_files_and_pve):
1287
+        caplog.set_level(logging.DEBUG, logger=MODULE_LOGGER_NAME)
1288
+        status = self.f.download_file_to_storage(
1289
+            "https://sub.domain.tld/file.iso", checksum_type="asdf"
1290
+        )
1291
+
1292
+        assert status is None
1293
+
1294
+        assert caplog.record_tuples == [
1295
+            (
1296
+                MODULE_LOGGER_NAME,
1297
+                logging.ERROR,
1298
+                "Must pass both checksum and checksum_type or leave both None for auto-discovery",
1299
+            ),
1300
+        ]
1301
+
1302
+    def test_get_file_info_missing(self, mock_pve):
1303
+        f = Files(self.prox, "node1", "storage1")
1304
+        info = f.get_file_info("https://sub.domain.tld/missing.iso")
1305
+
1306
+        assert info is None
1307
+
1308
+    def test_get_file_info_non_iso(self, mock_pve):
1309
+        f = Files(self.prox, "node1", "storage1")
1310
+        info = f.get_file_info("https://sub.domain.tld/index.html")
1311
+
1312
+        assert info["filename"] == "index.html"
1313
+        assert info["mimetype"] == "text/html"
1314
+
1315
+
1316
+class TestFilesUpload:
1317
+    prox = ProxmoxAPI("1.2.3.4:1234", token_name="name", token_value="value")
1318
+    f = Files(prox, "node1", "storage1")
1319
+
1320
+    def test_upload_no_file(self, mock_files_and_pve, caplog):
1321
+        status = self.f.upload_local_file_to_storage("/does-not-exist.iso")
1322
+
1323
+        assert status is None
1324
+        assert caplog.record_tuples == [
1325
+            (
1326
+                MODULE_LOGGER_NAME,
1327
+                logging.ERROR,
1328
+                '"/does-not-exist.iso" does not exist or is not a file',
1329
+            ),
1330
+        ]
1331
+
1332
+    def test_upload_dir(self, mock_files_and_pve, caplog):
1333
+        with tempfile.TemporaryDirectory() as tmp_dir:
1334
+            status = self.f.upload_local_file_to_storage(tmp_dir)
1335
+
1336
+            assert status is None
1337
+            assert caplog.record_tuples == [
1338
+                (
1339
+                    MODULE_LOGGER_NAME,
1340
+                    logging.ERROR,
1341
+                    f'"{tmp_dir}" does not exist or is not a file',
1342
+                ),
1343
+            ]
1344
+
1345
+    def test_upload_empty_file(self, mock_files_and_pve, caplog):
1346
+        with tempfile.NamedTemporaryFile("rb") as f_obj:
1347
+            status = self.f.upload_local_file_to_storage(filename=f_obj.name)
1348
+
1349
+            assert status is not None
1350
+            assert caplog.record_tuples == []
1351
+
1352
+    def test_upload_non_empty_file(self, mock_files_and_pve, caplog):
1353
+        with tempfile.NamedTemporaryFile("w+b") as f_obj:
1354
+            f_obj.write(b"a" * 100)
1355
+            f_obj.seek(0)
1356
+            status = self.f.upload_local_file_to_storage(filename=f_obj.name)
1357
+
1358
+            assert status is not None
1359
+            assert caplog.record_tuples == []
1360
+
1361
+    def test_upload_no_checksum(self, mock_files_and_pve, caplog):
1362
+        with tempfile.NamedTemporaryFile("rb") as f_obj:
1363
+            status = self.f.upload_local_file_to_storage(
1364
+                filename=f_obj.name, do_checksum_check=False
1365
+            )
1366
+
1367
+            assert status is not None
1368
+            assert caplog.record_tuples == []
1369
+
1370
+    def test_upload_checksum_unavailable(self, mock_files_and_pve, caplog, apply_no_checksums):
1371
+        with tempfile.NamedTemporaryFile("rb") as f_obj:
1372
+            status = self.f.upload_local_file_to_storage(filename=f_obj.name)
1373
+
1374
+            assert status is not None
1375
+            assert caplog.record_tuples == [
1376
+                (
1377
+                    MODULE_LOGGER_NAME,
1378
+                    logging.WARNING,
1379
+                    "There are no Proxmox supported checksums which are supported by hashlib. Skipping checksum validation",
1380
+                )
1381
+            ]
1382
+
1383
+    def test_upload_non_blocking(self, mock_files_and_pve, caplog):
1384
+        with tempfile.NamedTemporaryFile("rb") as f_obj:
1385
+            status = self.f.upload_local_file_to_storage(filename=f_obj.name, blocking_status=False)
1386
+
1387
+            assert status is not None
1388
+            assert caplog.record_tuples == []
1389
+
1390
+    def test_upload_proxmox_error(self, mock_files_and_pve, caplog):
1391
+        with tempfile.NamedTemporaryFile("rb") as f_obj:
1392
+            f_copy = Files(self.f._prox, self.f._node, "missing")
1393
+
1394
+            with pytest.raises(core.ResourceException) as exc_info:
1395
+                f_copy.upload_local_file_to_storage(filename=f_obj.name)
1396
+
1397
+            assert exc_info.value.status_code == 500
1398
+            assert exc_info.value.status_message == "Internal Server Error"
1399
+            # assert exc_info.value.content == "storage 'missing' does not exist"
1400
+
1401
+    def test_upload_io_error(self, mock_files_and_pve, caplog):
1402
+        with tempfile.NamedTemporaryFile("rb") as f_obj:
1403
+            mo = mock.mock_open()
1404
+            mo.side_effect = IOError("ERROR MESSAGE")
1405
+            with mock.patch("builtins.open", mo):
1406
+                status = self.f.upload_local_file_to_storage(filename=f_obj.name)
1407
+
1408
+            assert status is None
1409
+            assert caplog.record_tuples == [(MODULE_LOGGER_NAME, logging.ERROR, "ERROR MESSAGE")]
1410
+
1411
+
1412
+@pytest.fixture
1413
+def apply_no_checksums():
1414
+    with mock.patch("hashlib.algorithms_available", set()):
1415
+        yield
1416
diff -ruN tests/tools/test_tasks.py proxmoxer-2.2.0/tests/tools/test_tasks.py
1417
--- tests/tools/test_tasks.py	1970-01-01 01:00:00.000000000 +0100
1418
+++ proxmoxer-2.2.0/tests/tools/test_tasks.py	2024-12-15 02:12:42.000000000 +0000
1419
@@ -0,0 +1,223 @@
1420
+__author__ = "John Hollowell"
1421
+__copyright__ = "(c) John Hollowell 2022"
1422
+__license__ = "MIT"
1423
+
1424
+import logging
1425
+
1426
+import pytest
1427
+
1428
+from proxmoxer import ProxmoxAPI
1429
+from proxmoxer.tools import Tasks
1430
+
1431
+from ..api_mock import mock_pve  # pylint: disable=unused-import # noqa: F401
1432
+
1433
+
1434
+class TestBlockingStatus:
1435
+    def test_basic(self, mocked_prox, caplog):
1436
+        caplog.set_level(logging.DEBUG, logger="proxmoxer.core")
1437
+
1438
+        status = Tasks.blocking_status(
1439
+            mocked_prox, "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:done"
1440
+        )
1441
+
1442
+        assert status == {
1443
+            "upid": "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:done",
1444
+            "starttime": 1661825068,
1445
+            "user": "root@pam",
1446
+            "type": "vzdump",
1447
+            "pstart": 284768076,
1448
+            "status": "stopped",
1449
+            "exitstatus": "OK",
1450
+            "pid": 1044989,
1451
+            "id": "110",
1452
+            "node": "node1",
1453
+        }
1454
+        assert caplog.record_tuples == [
1455
+            (
1456
+                "proxmoxer.core",
1457
+                20,
1458
+                "GET https://1.2.3.4:1234/api2/json/nodes/node1/tasks/UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:done/status",
1459
+            ),
1460
+            (
1461
+                "proxmoxer.core",
1462
+                10,
1463
+                'Status code: 200, output: b\'{"data": {"upid": "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:done", "starttime": 1661825068, "user": "root@pam", "type": "vzdump", "pstart": 284768076, "status": "stopped", "exitstatus": "OK", "pid": 1044989, "id": "110", "node": "node1"}}\'',
1464
+            ),
1465
+        ]
1466
+
1467
+    def test_zeroed(self, mocked_prox, caplog):
1468
+        caplog.set_level(logging.DEBUG, logger="proxmoxer.core")
1469
+
1470
+        status = Tasks.blocking_status(
1471
+            mocked_prox, "UPID:node:00000000:00000000:00000000:task:id:root@pam:comment"
1472
+        )
1473
+
1474
+        assert status == {
1475
+            "upid": "UPID:node:00000000:00000000:00000000:task:id:root@pam:comment",
1476
+            "node": "node",
1477
+            "pid": 0,
1478
+            "pstart": 0,
1479
+            "starttime": 0,
1480
+            "type": "task",
1481
+            "id": "id",
1482
+            "user": "root@pam",
1483
+            "status": "stopped",
1484
+            "exitstatus": "OK",
1485
+        }
1486
+        assert caplog.record_tuples == [
1487
+            (
1488
+                "proxmoxer.core",
1489
+                20,
1490
+                "GET https://1.2.3.4:1234/api2/json/nodes/node/tasks/UPID:node:00000000:00000000:00000000:task:id:root@pam:comment/status",
1491
+            ),
1492
+            (
1493
+                "proxmoxer.core",
1494
+                10,
1495
+                'Status code: 200, output: b\'{"data": {"upid": "UPID:node:00000000:00000000:00000000:task:id:root@pam:comment", "node": "node", "pid": 0, "pstart": 0, "starttime": 0, "type": "task", "id": "id", "user": "root@pam", "status": "stopped", "exitstatus": "OK"}}\'',
1496
+            ),
1497
+        ]
1498
+
1499
+    def test_killed(self, mocked_prox, caplog):
1500
+        caplog.set_level(logging.DEBUG, logger="proxmoxer.core")
1501
+
1502
+        status = Tasks.blocking_status(
1503
+            mocked_prox, "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:stopped"
1504
+        )
1505
+
1506
+        assert status == {
1507
+            "upid": "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:stopped",
1508
+            "starttime": 1661825068,
1509
+            "user": "root@pam",
1510
+            "type": "vzdump",
1511
+            "pstart": 284768076,
1512
+            "status": "stopped",
1513
+            "exitstatus": "interrupted by signal",
1514
+            "pid": 1044989,
1515
+            "id": "110",
1516
+            "node": "node1",
1517
+        }
1518
+        assert caplog.record_tuples == [
1519
+            (
1520
+                "proxmoxer.core",
1521
+                20,
1522
+                "GET https://1.2.3.4:1234/api2/json/nodes/node1/tasks/UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:stopped/status",
1523
+            ),
1524
+            (
1525
+                "proxmoxer.core",
1526
+                10,
1527
+                'Status code: 200, output: b\'{"data": {"upid": "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:stopped", "starttime": 1661825068, "user": "root@pam", "type": "vzdump", "pstart": 284768076, "status": "stopped", "exitstatus": "interrupted by signal", "pid": 1044989, "id": "110", "node": "node1"}}\'',
1528
+            ),
1529
+        ]
1530
+
1531
+    def test_timeout(self, mocked_prox, caplog):
1532
+        caplog.set_level(logging.DEBUG, logger="proxmoxer.core")
1533
+
1534
+        status = Tasks.blocking_status(
1535
+            mocked_prox,
1536
+            "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:keep-running",
1537
+            timeout=0.021,
1538
+            polling_interval=0.01,
1539
+        )
1540
+
1541
+        assert status is None
1542
+        assert caplog.record_tuples == [
1543
+            (
1544
+                "proxmoxer.core",
1545
+                20,
1546
+                "GET https://1.2.3.4:1234/api2/json/nodes/node1/tasks/UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:keep-running/status",
1547
+            ),
1548
+            (
1549
+                "proxmoxer.core",
1550
+                10,
1551
+                'Status code: 200, output: b\'{"data": {"id": "110", "pid": 1044989, "node": "node1", "pstart": 284768076, "status": "running", "upid": "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:keep-running", "starttime": 1661825068, "user": "root@pam", "type": "vzdump"}}\'',
1552
+            ),
1553
+            (
1554
+                "proxmoxer.core",
1555
+                20,
1556
+                "GET https://1.2.3.4:1234/api2/json/nodes/node1/tasks/UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:keep-running/status",
1557
+            ),
1558
+            (
1559
+                "proxmoxer.core",
1560
+                10,
1561
+                'Status code: 200, output: b\'{"data": {"id": "110", "pid": 1044989, "node": "node1", "pstart": 284768076, "status": "running", "upid": "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:keep-running", "starttime": 1661825068, "user": "root@pam", "type": "vzdump"}}\'',
1562
+            ),
1563
+            (
1564
+                "proxmoxer.core",
1565
+                20,
1566
+                "GET https://1.2.3.4:1234/api2/json/nodes/node1/tasks/UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:keep-running/status",
1567
+            ),
1568
+            (
1569
+                "proxmoxer.core",
1570
+                10,
1571
+                'Status code: 200, output: b\'{"data": {"id": "110", "pid": 1044989, "node": "node1", "pstart": 284768076, "status": "running", "upid": "UPID:node1:000FF1FD:10F9374C:630D702C:vzdump:110:root@pam:keep-running", "starttime": 1661825068, "user": "root@pam", "type": "vzdump"}}\'',
1572
+            ),
1573
+        ]
1574
+
1575
+
1576
+class TestDecodeUpid:
1577
+    def test_basic(self):
1578
+        upid = "UPID:node:000CFC5C:03E8D0C3:6194806C:aptupdate::root@pam:"
1579
+        decoded = Tasks.decode_upid(upid)
1580
+
1581
+        assert decoded["upid"] == upid
1582
+        assert decoded["node"] == "node"
1583
+        assert decoded["pid"] == 851036
1584
+        assert decoded["pstart"] == 65589443
1585
+        assert decoded["starttime"] == 1637122156
1586
+        assert decoded["type"] == "aptupdate"
1587
+        assert decoded["id"] == ""
1588
+        assert decoded["user"] == "root@pam"
1589
+        assert decoded["comment"] == ""
1590
+
1591
+    def test_all_values(self):
1592
+        upid = "UPID:node1:000CFFFA:03E8EF53:619480BA:vzdump:103:root@pam:local"
1593
+        decoded = Tasks.decode_upid(upid)
1594
+
1595
+        assert decoded["upid"] == upid
1596
+        assert decoded["node"] == "node1"
1597
+        assert decoded["pid"] == 851962
1598
+        assert decoded["pstart"] == 65597267
1599
+        assert decoded["starttime"] == 1637122234
1600
+        assert decoded["type"] == "vzdump"
1601
+        assert decoded["id"] == "103"
1602
+        assert decoded["user"] == "root@pam"
1603
+        assert decoded["comment"] == "local"
1604
+
1605
+    def test_invalid_length(self):
1606
+        upid = "UPID:node1:000CFFFA:03E8EF53:619480BA:vzdump:103:root@pam"
1607
+        with pytest.raises(AssertionError) as exc_info:
1608
+            Tasks.decode_upid(upid)
1609
+
1610
+        assert str(exc_info.value) == "UPID is not in the correct format"
1611
+
1612
+    def test_invalid_start(self):
1613
+        upid = "ASDF:node1:000CFFFA:03E8EF53:619480BA:vzdump:103:root@pam:"
1614
+        with pytest.raises(AssertionError) as exc_info:
1615
+            Tasks.decode_upid(upid)
1616
+
1617
+        assert str(exc_info.value) == "UPID is not in the correct format"
1618
+
1619
+
1620
+class TestDecodeLog:
1621
+    def test_basic(self):
1622
+        log_list = [{"n": 1, "t": "client connection: 127.0.0.1:49608"}, {"t": "TASK OK", "n": 2}]
1623
+        log_str = Tasks.decode_log(log_list)
1624
+
1625
+        assert log_str == "client connection: 127.0.0.1:49608\nTASK OK"
1626
+
1627
+    def test_empty(self):
1628
+        log_list = []
1629
+        log_str = Tasks.decode_log(log_list)
1630
+
1631
+        assert log_str == ""
1632
+
1633
+    def test_unordered(self):
1634
+        log_list = [{"n": 3, "t": "third"}, {"t": "first", "n": 1}, {"t": "second", "n": 2}]
1635
+        log_str = Tasks.decode_log(log_list)
1636
+
1637
+        assert log_str == "first\nsecond\nthird"
1638
+
1639
+
1640
+@pytest.fixture
1641
+def mocked_prox(mock_pve):
1642
+    return ProxmoxAPI("1.2.3.4:1234", user="user", password="password")

Return to bug 283360