canvas 0.32.0__py3-none-any.whl → 0.33.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.
Potentially problematic release.
This version of canvas might be problematic. Click here for more details.
- {canvas-0.32.0.dist-info → canvas-0.33.1.dist-info}/METADATA +2 -1
- canvas-0.33.1.dist-info/RECORD +272 -0
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +4 -0
- canvas_sdk/__init__.py +3 -0
- canvas_sdk/commands/__init__.py +1 -1
- canvas_sdk/commands/base.py +3 -0
- canvas_sdk/commands/commands/__init__.py +1 -0
- canvas_sdk/commands/commands/adjust_prescription.py +3 -0
- canvas_sdk/commands/commands/allergy.py +7 -0
- canvas_sdk/commands/commands/assess.py +2 -0
- canvas_sdk/commands/commands/close_goal.py +3 -0
- canvas_sdk/commands/commands/diagnose.py +3 -0
- canvas_sdk/commands/commands/exam.py +3 -0
- canvas_sdk/commands/commands/family_history.py +3 -0
- canvas_sdk/commands/commands/follow_up.py +3 -0
- canvas_sdk/commands/commands/goal.py +3 -0
- canvas_sdk/commands/commands/history_present_illness.py +3 -0
- canvas_sdk/commands/commands/imaging_order.py +3 -0
- canvas_sdk/commands/commands/instruct.py +3 -0
- canvas_sdk/commands/commands/lab_order.py +3 -0
- canvas_sdk/commands/commands/medical_history.py +3 -0
- canvas_sdk/commands/commands/medication_statement.py +2 -0
- canvas_sdk/commands/commands/past_surgical_history.py +3 -0
- canvas_sdk/commands/commands/perform.py +3 -0
- canvas_sdk/commands/commands/plan.py +3 -0
- canvas_sdk/commands/commands/prescribe.py +8 -0
- canvas_sdk/commands/commands/questionnaire/__init__.py +3 -13
- canvas_sdk/commands/commands/questionnaire/question.py +10 -0
- canvas_sdk/commands/commands/reason_for_visit.py +3 -0
- canvas_sdk/commands/commands/refer.py +3 -0
- canvas_sdk/commands/commands/refill.py +3 -0
- canvas_sdk/commands/commands/remove_allergy.py +3 -0
- canvas_sdk/commands/commands/resolve_condition.py +3 -0
- canvas_sdk/commands/commands/review_of_systems.py +3 -0
- canvas_sdk/commands/commands/stop_medication.py +3 -0
- canvas_sdk/commands/commands/structured_assessment.py +3 -0
- canvas_sdk/commands/commands/task.py +7 -0
- canvas_sdk/commands/commands/update_diagnosis.py +3 -0
- canvas_sdk/commands/commands/update_goal.py +3 -0
- canvas_sdk/commands/commands/vitals.py +3 -0
- canvas_sdk/commands/constants.py +8 -0
- canvas_sdk/effects/__init__.py +1 -1
- canvas_sdk/effects/banner_alert/__init__.py +1 -1
- canvas_sdk/effects/banner_alert/add_banner_alert.py +3 -0
- canvas_sdk/effects/banner_alert/remove_banner_alert.py +3 -0
- canvas_sdk/effects/base.py +7 -0
- canvas_sdk/effects/billing_line_item/__init__.py +5 -1
- canvas_sdk/effects/billing_line_item/add_billing_line_item.py +3 -0
- canvas_sdk/effects/billing_line_item/remove_billing_line_item.py +3 -0
- canvas_sdk/effects/billing_line_item/update_billing_line_item.py +3 -0
- canvas_sdk/effects/launch_modal.py +3 -0
- canvas_sdk/effects/patient_chart_summary_configuration.py +3 -0
- canvas_sdk/effects/patient_portal/__init__.py +1 -0
- canvas_sdk/effects/patient_portal/application_configuration.py +3 -0
- canvas_sdk/effects/patient_portal/form_result.py +3 -0
- canvas_sdk/effects/patient_portal_menu_configuration.py +3 -0
- canvas_sdk/effects/patient_profile_configuration.py +6 -1
- canvas_sdk/effects/protocol_card/__init__.py +1 -1
- canvas_sdk/effects/protocol_card/protocol_card.py +6 -0
- canvas_sdk/effects/questionnaire_result.py +3 -0
- canvas_sdk/effects/send_invite.py +46 -0
- canvas_sdk/effects/show_button.py +3 -0
- canvas_sdk/effects/simple_api.py +9 -0
- canvas_sdk/effects/surescripts/__init__.py +2 -2
- canvas_sdk/effects/surescripts/surescripts_messages.py +7 -0
- canvas_sdk/effects/task/__init__.py +6 -1
- canvas_sdk/effects/task/task.py +8 -0
- canvas_sdk/effects/update_user.py +81 -0
- canvas_sdk/effects/widgets/__init__.py +1 -1
- canvas_sdk/effects/widgets/portal_widget.py +3 -0
- canvas_sdk/events/__init__.py +6 -1
- canvas_sdk/events/base.py +3 -0
- canvas_sdk/handlers/__init__.py +1 -1
- canvas_sdk/handlers/action_button.py +6 -0
- canvas_sdk/handlers/application.py +3 -0
- canvas_sdk/handlers/base.py +3 -0
- canvas_sdk/handlers/cron_task.py +3 -0
- canvas_sdk/handlers/simple_api/__init__.py +3 -2
- canvas_sdk/handlers/simple_api/api.py +26 -1
- canvas_sdk/handlers/simple_api/exceptions.py +10 -0
- canvas_sdk/handlers/simple_api/security.py +21 -5
- canvas_sdk/handlers/simple_api/tools.py +9 -0
- canvas_sdk/protocols/__init__.py +1 -1
- canvas_sdk/protocols/base.py +3 -0
- canvas_sdk/protocols/clinical_quality_measure.py +6 -1
- canvas_sdk/protocols/timeframe.py +3 -0
- canvas_sdk/questionnaires/__init__.py +1 -1
- canvas_sdk/questionnaires/utils.py +7 -0
- canvas_sdk/templates/__init__.py +1 -1
- canvas_sdk/templates/utils.py +3 -0
- canvas_sdk/utils/__init__.py +1 -1
- canvas_sdk/utils/http.py +69 -35
- canvas_sdk/utils/plugins.py +4 -0
- canvas_sdk/utils/stats.py +11 -0
- canvas_sdk/v1/__init__.py +1 -0
- canvas_sdk/v1/apps.py +3 -0
- canvas_sdk/v1/data/__init__.py +2 -2
- canvas_sdk/v1/data/allergy_intolerance.py +3 -0
- canvas_sdk/v1/data/appointment.py +7 -0
- canvas_sdk/v1/data/assessment.py +3 -0
- canvas_sdk/v1/data/banner_alert.py +3 -0
- canvas_sdk/v1/data/base.py +3 -0
- canvas_sdk/v1/data/billing.py +7 -0
- canvas_sdk/v1/data/care_team.py +7 -0
- canvas_sdk/v1/data/command.py +3 -0
- canvas_sdk/v1/data/common.py +18 -0
- canvas_sdk/v1/data/condition.py +7 -0
- canvas_sdk/v1/data/coverage.py +14 -0
- canvas_sdk/v1/data/detected_issue.py +3 -0
- canvas_sdk/v1/data/device.py +3 -0
- canvas_sdk/v1/data/imaging.py +7 -0
- canvas_sdk/v1/data/lab.py +16 -0
- canvas_sdk/v1/data/medication.py +3 -0
- canvas_sdk/v1/data/note.py +9 -0
- canvas_sdk/v1/data/observation.py +9 -0
- canvas_sdk/v1/data/organization.py +3 -0
- canvas_sdk/v1/data/patient.py +20 -3
- canvas_sdk/v1/data/practicelocation.py +7 -0
- canvas_sdk/v1/data/protocol_override.py +7 -0
- canvas_sdk/v1/data/questionnaire.py +16 -3
- canvas_sdk/v1/data/reason_for_visit.py +3 -0
- canvas_sdk/v1/data/staff.py +3 -0
- canvas_sdk/v1/data/task.py +12 -0
- canvas_sdk/v1/data/team.py +8 -1
- canvas_sdk/v1/data/user.py +5 -1
- canvas_sdk/v1/models.py +2 -0
- canvas_sdk/value_set/__init__.py +1 -0
- canvas_sdk/value_set/_utilities.py +16 -0
- canvas_sdk/value_set/custom.py +4 -0
- canvas_sdk/value_set/hcc2018.py +3 -0
- canvas_sdk/value_set/v2022/__init__.py +1 -0
- canvas_sdk/value_set/v2022/adverse_event.py +3 -0
- canvas_sdk/value_set/v2022/allergy.py +5 -0
- canvas_sdk/value_set/v2022/assessment.py +5 -0
- canvas_sdk/value_set/v2022/communication.py +5 -0
- canvas_sdk/value_set/v2022/condition.py +5 -0
- canvas_sdk/value_set/v2022/device.py +5 -0
- canvas_sdk/value_set/v2022/diagnostic_study.py +5 -0
- canvas_sdk/value_set/v2022/encounter.py +5 -0
- canvas_sdk/value_set/v2022/immunization.py +5 -0
- canvas_sdk/value_set/v2022/individual_characteristic.py +5 -0
- canvas_sdk/value_set/v2022/intervention.py +5 -0
- canvas_sdk/value_set/v2022/laboratory_test.py +5 -0
- canvas_sdk/value_set/v2022/medication.py +5 -0
- canvas_sdk/value_set/v2022/physical_exam.py +5 -0
- canvas_sdk/value_set/v2022/procedure.py +5 -0
- canvas_sdk/value_set/v2022/symptom.py +3 -0
- canvas_sdk/value_set/value_set.py +9 -0
- canvas_sdk/views/__init__.py +1 -0
- logger/__init__.py +2 -0
- logger/logger.py +3 -0
- plugin_runner/aws_headers.py +1 -1
- plugin_runner/load_all_plugins.py +202 -0
- plugin_runner/plugin_runner.py +26 -24
- plugin_runner/sandbox.py +497 -115
- protobufs/canvas_generated/messages/effects.proto +3 -0
- settings.py +5 -2
- canvas-0.32.0.dist-info/RECORD +0 -364
- canvas_cli/apps/auth/tests.py +0 -155
- canvas_cli/apps/plugin/tests.py +0 -85
- canvas_cli/conftest.py +0 -28
- canvas_cli/tests.py +0 -217
- canvas_cli/utils/context/tests.py +0 -131
- canvas_cli/utils/print/tests.py +0 -69
- canvas_cli/utils/urls/tests.py +0 -12
- canvas_cli/utils/validators/tests.py +0 -37
- canvas_sdk/commands/tests/protocol/__init__.py +0 -0
- canvas_sdk/commands/tests/protocol/tests.py +0 -83
- canvas_sdk/commands/tests/schema/__init__.py +0 -0
- canvas_sdk/commands/tests/schema/tests.py +0 -108
- canvas_sdk/commands/tests/test_base_command.py +0 -81
- canvas_sdk/commands/tests/test_utils.py +0 -375
- canvas_sdk/commands/tests/unit/__init__.py +0 -0
- canvas_sdk/commands/tests/unit/tests.py +0 -278
- canvas_sdk/effects/banner_alert/tests.py +0 -288
- canvas_sdk/effects/protocol_card/tests.py +0 -191
- canvas_sdk/questionnaires/tests/__init__.py +0 -0
- canvas_sdk/questionnaires/tests/test_utils.py +0 -74
- canvas_sdk/templates/tests/__init__.py +0 -0
- canvas_sdk/templates/tests/test_utils.py +0 -43
- canvas_sdk/tests/__init__.py +0 -0
- canvas_sdk/tests/handlers/__init__.py +0 -0
- canvas_sdk/tests/handlers/test_simple_api.py +0 -1167
- canvas_sdk/utils/tests.py +0 -72
- canvas_sdk/value_set/tests/test_value_sets.py +0 -72
- plugin_runner/tests/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/example_plugin/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/example_plugin/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/example_plugin/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/example_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/example_plugin/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/CANVAS_MANIFEST.json +0 -38
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/README.md +0 -11
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/my_protocol.py +0 -33
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/__init__.py +0 -3
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/base.py +0 -6
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/__init__.py +0 -5
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/base.py +0 -4
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/CANVAS_MANIFEST.json +0 -52
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/README.md +0 -11
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/protocols/my_protocol.py +0 -39
- plugin_runner/tests/fixtures/plugins/test_load_questionnaire/questionnaires/example_questionnaire.yml +0 -61
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/base.py +0 -10
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/base.py +0 -10
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/other_module/base.py +0 -3
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v1/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/other_module/base.py +0 -6
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v2/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/other_module/base.py +0 -8
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_outside_plugin_v3/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/CANVAS_MANIFEST.json +0 -29
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/README.md +0 -12
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/other_module/base.py +0 -3
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_imports_plugin/protocols/my_protocol.py +0 -18
- plugin_runner/tests/fixtures/plugins/test_render_template/CANVAS_MANIFEST.json +0 -47
- plugin_runner/tests/fixtures/plugins/test_render_template/README.md +0 -11
- plugin_runner/tests/fixtures/plugins/test_render_template/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_render_template/protocols/my_protocol.py +0 -43
- plugin_runner/tests/fixtures/plugins/test_render_template/templates/template.html +0 -10
- plugin_runner/tests/fixtures/plugins/test_simple_api/CANVAS_MANIFEST.json +0 -47
- plugin_runner/tests/fixtures/plugins/test_simple_api/README.md +0 -11
- plugin_runner/tests/fixtures/plugins/test_simple_api/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_simple_api/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_simple_api/protocols/my_protocol.py +0 -43
- plugin_runner/tests/test_application.py +0 -65
- plugin_runner/tests/test_plugin_installer.py +0 -127
- plugin_runner/tests/test_plugin_runner.py +0 -388
- plugin_runner/tests/test_sandbox.py +0 -137
- {canvas-0.32.0.dist-info → canvas-0.33.1.dist-info}/WHEEL +0 -0
- {canvas-0.32.0.dist-info → canvas-0.33.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import tarfile
|
|
3
|
-
import tempfile
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from unittest.mock import MagicMock, patch
|
|
6
|
-
|
|
7
|
-
from pytest_mock import MockerFixture
|
|
8
|
-
|
|
9
|
-
from plugin_runner.installation import (
|
|
10
|
-
PluginAttributes,
|
|
11
|
-
_extract_rows_to_dict,
|
|
12
|
-
download_plugin,
|
|
13
|
-
install_plugins,
|
|
14
|
-
uninstall_plugin,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _create_tarball(name: str) -> Path:
|
|
19
|
-
# Create a temporary tarball file
|
|
20
|
-
temp_dir = tempfile.mkdtemp()
|
|
21
|
-
tarball_path = Path(temp_dir) / f"{name}.tar.gz"
|
|
22
|
-
|
|
23
|
-
# Add some files to the tarball
|
|
24
|
-
with tarfile.open(tarball_path, "w:gz") as tar:
|
|
25
|
-
for i in range(3):
|
|
26
|
-
file_path = Path(temp_dir) / f"file{i}.txt"
|
|
27
|
-
file_path.write_text(f"Content of file {i}")
|
|
28
|
-
tar.add(file_path, arcname=f"file{i}.txt")
|
|
29
|
-
|
|
30
|
-
# Return a Path handle to the tarball
|
|
31
|
-
return tarball_path
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def test_extract_rows_to_dict() -> None:
|
|
35
|
-
"""Test that database rows can be extracted to a dictionary with secrets appropriately attributed to plugin."""
|
|
36
|
-
rows = [
|
|
37
|
-
{
|
|
38
|
-
"name": "plugin1",
|
|
39
|
-
"version": "1.0",
|
|
40
|
-
"package": "package1",
|
|
41
|
-
"key": "key1",
|
|
42
|
-
"value": "value1",
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"name": "plugin1",
|
|
46
|
-
"version": "1.0",
|
|
47
|
-
"package": "package1",
|
|
48
|
-
"key": "key2",
|
|
49
|
-
"value": "value2",
|
|
50
|
-
},
|
|
51
|
-
{"name": "plugin2", "version": "2.0", "package": "package2", "key": None, "value": None},
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
expected_output = {
|
|
55
|
-
"plugin1": {
|
|
56
|
-
"version": "1.0",
|
|
57
|
-
"package": "package1",
|
|
58
|
-
"secrets": {"key1": "value1", "key2": "value2"},
|
|
59
|
-
},
|
|
60
|
-
"plugin2": {
|
|
61
|
-
"version": "2.0",
|
|
62
|
-
"package": "package2",
|
|
63
|
-
"secrets": {},
|
|
64
|
-
},
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
result = _extract_rows_to_dict(rows)
|
|
68
|
-
assert result == expected_output
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def test_plugin_installation_from_tarball(mocker: MockerFixture) -> None:
|
|
72
|
-
"""Test that plugins can be installed from tarballs."""
|
|
73
|
-
mock_plugins = {
|
|
74
|
-
"plugin1": PluginAttributes(
|
|
75
|
-
version="1.0", package="plugins/plugin1.tar.gz", secrets={"key1": "value1"}
|
|
76
|
-
),
|
|
77
|
-
"plugin2": PluginAttributes(
|
|
78
|
-
version="1.0", package="plugins/plugin2.tar", secrets={"key2": "value2"}
|
|
79
|
-
),
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
tarball_1 = _create_tarball("plugin1")
|
|
83
|
-
tarball_2 = _create_tarball("plugin2")
|
|
84
|
-
|
|
85
|
-
mocker.patch("plugin_runner.installation.enabled_plugins", return_value=mock_plugins)
|
|
86
|
-
|
|
87
|
-
def mock_download_plugin(package: str) -> MagicMock:
|
|
88
|
-
mock_context = mocker.Mock()
|
|
89
|
-
if package == "plugins/plugin1.tar.gz":
|
|
90
|
-
mock_context.__enter__ = mocker.Mock(return_value=tarball_1)
|
|
91
|
-
elif package == "plugins/plugin2.tar":
|
|
92
|
-
mock_context.__enter__ = mocker.Mock(return_value=tarball_2)
|
|
93
|
-
mock_context.__exit__ = mocker.Mock(return_value=None)
|
|
94
|
-
return mock_context
|
|
95
|
-
|
|
96
|
-
mocker.patch(
|
|
97
|
-
"plugin_runner.installation.download_plugin",
|
|
98
|
-
side_effect=mock_download_plugin,
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
install_plugins()
|
|
102
|
-
assert Path("plugin_runner/tests/data/plugins/plugin1").exists()
|
|
103
|
-
assert Path("plugin_runner/tests/data/plugins/plugin1/SECRETS.json").exists()
|
|
104
|
-
with open("plugin_runner/tests/data/plugins/plugin1/SECRETS.json") as f:
|
|
105
|
-
assert json.load(f) == mock_plugins["plugin1"]["secrets"]
|
|
106
|
-
assert Path("plugin_runner/tests/data/plugins/plugin2").exists()
|
|
107
|
-
assert Path("plugin_runner/tests/data/plugins/plugin2/SECRETS.json").exists()
|
|
108
|
-
with open("plugin_runner/tests/data/plugins/plugin2/SECRETS.json") as f:
|
|
109
|
-
assert json.load(f) == mock_plugins["plugin2"]["secrets"]
|
|
110
|
-
|
|
111
|
-
uninstall_plugin("plugin1")
|
|
112
|
-
uninstall_plugin("plugin2")
|
|
113
|
-
assert not Path("plugin_runner/tests/data/plugins/plugin1").exists()
|
|
114
|
-
assert not Path("plugin_runner/tests/data/plugins/plugin2").exists()
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def test_download(mocker: MockerFixture) -> None:
|
|
118
|
-
"""Test that the plugin package can be written to disk, mocking out S3."""
|
|
119
|
-
mock_response = MagicMock()
|
|
120
|
-
mock_response.status_code = 200
|
|
121
|
-
mock_response.content = b"some content in a file"
|
|
122
|
-
with patch("requests.request", return_value=mock_response) as mock_request:
|
|
123
|
-
plugin_package = "plugins/plugin1.tar.gz"
|
|
124
|
-
with download_plugin(plugin_package) as plugin_path:
|
|
125
|
-
assert plugin_path.exists()
|
|
126
|
-
assert plugin_path.read_bytes() == b"some content in a file"
|
|
127
|
-
mock_request.assert_called_once()
|
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import json
|
|
3
|
-
import logging
|
|
4
|
-
import pickle
|
|
5
|
-
import shutil
|
|
6
|
-
from base64 import b64encode
|
|
7
|
-
from http import HTTPStatus
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Any
|
|
10
|
-
from unittest.mock import AsyncMock, MagicMock, patch
|
|
11
|
-
|
|
12
|
-
import pytest
|
|
13
|
-
|
|
14
|
-
from canvas_generated.messages.effects_pb2 import EffectType
|
|
15
|
-
from canvas_generated.messages.plugins_pb2 import ReloadPluginsRequest
|
|
16
|
-
from canvas_sdk.effects.simple_api import Response
|
|
17
|
-
from canvas_sdk.events import Event, EventRequest, EventType
|
|
18
|
-
from plugin_runner.plugin_runner import (
|
|
19
|
-
EVENT_HANDLER_MAP,
|
|
20
|
-
LOADED_PLUGINS,
|
|
21
|
-
PluginRunner,
|
|
22
|
-
load_or_reload_plugin,
|
|
23
|
-
load_plugins,
|
|
24
|
-
synchronize_plugins,
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@pytest.fixture
|
|
29
|
-
def plugin_runner() -> PluginRunner:
|
|
30
|
-
"""Fixture to initialize PluginRunner with mocks."""
|
|
31
|
-
runner = PluginRunner()
|
|
32
|
-
runner.statsd_client = MagicMock() # type: ignore[attr-defined]
|
|
33
|
-
return runner
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
|
|
37
|
-
def test_load_plugins_with_valid_plugin(install_test_plugin: Path, load_test_plugins: None) -> None:
|
|
38
|
-
"""Test loading plugins with a valid plugin."""
|
|
39
|
-
assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" in LOADED_PLUGINS
|
|
40
|
-
assert (
|
|
41
|
-
LOADED_PLUGINS["example_plugin:example_plugin.protocols.my_protocol:Protocol"]["active"]
|
|
42
|
-
is True
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@pytest.mark.asyncio
|
|
47
|
-
@pytest.mark.parametrize("install_test_plugin", ["test_module_imports_plugin"], indirect=True)
|
|
48
|
-
async def test_load_plugins_with_plugin_that_imports_other_modules_within_plugin_package(
|
|
49
|
-
install_test_plugin: Path, plugin_runner: PluginRunner, load_test_plugins: None
|
|
50
|
-
) -> None:
|
|
51
|
-
"""Test loading plugins with a valid plugin that imports other modules within the current plugin package."""
|
|
52
|
-
assert (
|
|
53
|
-
"test_module_imports_plugin:test_module_imports_plugin.protocols.my_protocol:Protocol"
|
|
54
|
-
in LOADED_PLUGINS
|
|
55
|
-
)
|
|
56
|
-
assert (
|
|
57
|
-
LOADED_PLUGINS[
|
|
58
|
-
"test_module_imports_plugin:test_module_imports_plugin.protocols.my_protocol:Protocol"
|
|
59
|
-
]["active"]
|
|
60
|
-
is True
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
result = [
|
|
64
|
-
response
|
|
65
|
-
async for response in plugin_runner.HandleEvent(EventRequest(type=EventType.UNKNOWN), None)
|
|
66
|
-
]
|
|
67
|
-
|
|
68
|
-
assert len(result) == 1
|
|
69
|
-
assert result[0].success is True
|
|
70
|
-
assert len(result[0].effects) == 1
|
|
71
|
-
assert result[0].effects[0].type == EffectType.LOG
|
|
72
|
-
assert result[0].effects[0].payload == "Successfully imported!"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@pytest.mark.parametrize(
|
|
76
|
-
"install_test_plugin",
|
|
77
|
-
[
|
|
78
|
-
"test_module_imports_outside_plugin_v1",
|
|
79
|
-
"test_module_imports_outside_plugin_v2",
|
|
80
|
-
"test_module_imports_outside_plugin_v3",
|
|
81
|
-
],
|
|
82
|
-
indirect=True,
|
|
83
|
-
)
|
|
84
|
-
def test_load_plugins_with_plugin_that_imports_other_modules_outside_plugin_package(
|
|
85
|
-
install_test_plugin: Path, caplog: pytest.LogCaptureFixture
|
|
86
|
-
) -> None:
|
|
87
|
-
"""Test loading plugins with an invalid plugin that imports other modules outside the current plugin package."""
|
|
88
|
-
with caplog.at_level(logging.ERROR):
|
|
89
|
-
load_or_reload_plugin(install_test_plugin)
|
|
90
|
-
|
|
91
|
-
assert any("Error importing module" in record.message for record in caplog.records), (
|
|
92
|
-
"log.error() was not called with the expected message."
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
@pytest.mark.parametrize(
|
|
97
|
-
"install_test_plugin",
|
|
98
|
-
[
|
|
99
|
-
"test_module_forbidden_imports_plugin",
|
|
100
|
-
],
|
|
101
|
-
indirect=True,
|
|
102
|
-
)
|
|
103
|
-
def test_load_plugins_with_plugin_that_imports_forbidden_modules(
|
|
104
|
-
install_test_plugin: Path, caplog: pytest.LogCaptureFixture
|
|
105
|
-
) -> None:
|
|
106
|
-
"""Test loading plugins with an invalid plugin that imports forbidden modules."""
|
|
107
|
-
with caplog.at_level(logging.ERROR):
|
|
108
|
-
load_or_reload_plugin(install_test_plugin)
|
|
109
|
-
|
|
110
|
-
assert any("Error importing module" in record.message for record in caplog.records), (
|
|
111
|
-
"log.error() was not called with the expected message."
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
@pytest.mark.parametrize(
|
|
116
|
-
"install_test_plugin",
|
|
117
|
-
[
|
|
118
|
-
"test_module_forbidden_imports_runtime_plugin",
|
|
119
|
-
],
|
|
120
|
-
indirect=True,
|
|
121
|
-
)
|
|
122
|
-
def test_load_plugins_with_plugin_that_imports_forbidden_modules_at_runtime(
|
|
123
|
-
install_test_plugin: Path,
|
|
124
|
-
) -> None:
|
|
125
|
-
"""Test loading plugins with an invalid plugin that imports forbidden modules at runtime."""
|
|
126
|
-
with pytest.raises(ImportError, match="is not an allowed import."):
|
|
127
|
-
load_or_reload_plugin(install_test_plugin)
|
|
128
|
-
class_handler = LOADED_PLUGINS[
|
|
129
|
-
"test_module_forbidden_imports_runtime_plugin:test_module_forbidden_imports_runtime_plugin.protocols.my_protocol:Protocol"
|
|
130
|
-
]["class"]
|
|
131
|
-
class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
@pytest.mark.parametrize(
|
|
135
|
-
"install_test_plugin",
|
|
136
|
-
[
|
|
137
|
-
"test_implicit_imports_plugin",
|
|
138
|
-
],
|
|
139
|
-
indirect=True,
|
|
140
|
-
)
|
|
141
|
-
def test_plugin_that_implicitly_imports_allowed_modules(
|
|
142
|
-
install_test_plugin: Path, caplog: pytest.LogCaptureFixture
|
|
143
|
-
) -> None:
|
|
144
|
-
"""Test loading plugins with a plugin that implicitly imports allowed modules."""
|
|
145
|
-
with caplog.at_level(logging.INFO):
|
|
146
|
-
load_or_reload_plugin(install_test_plugin)
|
|
147
|
-
class_handler = LOADED_PLUGINS[
|
|
148
|
-
"test_implicit_imports_plugin:test_implicit_imports_plugin.protocols.my_protocol:Allowed"
|
|
149
|
-
]["class"]
|
|
150
|
-
class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
|
|
151
|
-
|
|
152
|
-
assert any("Hello, World!" in record.message for record in caplog.records), (
|
|
153
|
-
"log.info() with Template.render() was not called."
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
@pytest.mark.parametrize(
|
|
158
|
-
"install_test_plugin",
|
|
159
|
-
[
|
|
160
|
-
"test_implicit_imports_plugin",
|
|
161
|
-
],
|
|
162
|
-
indirect=True,
|
|
163
|
-
)
|
|
164
|
-
def test_plugin_that_implicitly_imports_forbidden_modules(
|
|
165
|
-
install_test_plugin: Path, caplog: pytest.LogCaptureFixture
|
|
166
|
-
) -> None:
|
|
167
|
-
"""Test loading plugins with an invalid plugin that implicitly imports forbidden modules."""
|
|
168
|
-
with (
|
|
169
|
-
caplog.at_level(logging.INFO),
|
|
170
|
-
pytest.raises(ImportError, match="'os' is not an allowed import."),
|
|
171
|
-
):
|
|
172
|
-
load_or_reload_plugin(install_test_plugin)
|
|
173
|
-
class_handler = LOADED_PLUGINS[
|
|
174
|
-
"test_implicit_imports_plugin:test_implicit_imports_plugin.protocols.my_protocol:Forbidden"
|
|
175
|
-
]["class"]
|
|
176
|
-
class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
|
|
177
|
-
|
|
178
|
-
assert any("os list dir" in record.message for record in caplog.records) is False, (
|
|
179
|
-
"log.info() with os.listdir() was called."
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
@pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
|
|
184
|
-
def test_reload_plugin(install_test_plugin: Path, load_test_plugins: None) -> None:
|
|
185
|
-
"""Test reloading a plugin."""
|
|
186
|
-
load_plugins()
|
|
187
|
-
|
|
188
|
-
assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" in LOADED_PLUGINS
|
|
189
|
-
assert (
|
|
190
|
-
LOADED_PLUGINS["example_plugin:example_plugin.protocols.my_protocol:Protocol"]["active"]
|
|
191
|
-
is True
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
@pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
|
|
196
|
-
def test_remove_plugin_should_be_removed_from_loaded_plugins(
|
|
197
|
-
install_test_plugin: Path, load_test_plugins: None
|
|
198
|
-
) -> None:
|
|
199
|
-
"""Test removing a plugin."""
|
|
200
|
-
assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" in LOADED_PLUGINS
|
|
201
|
-
shutil.rmtree(install_test_plugin)
|
|
202
|
-
load_plugins()
|
|
203
|
-
assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" not in LOADED_PLUGINS
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
@pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
|
|
207
|
-
@pytest.mark.parametrize("load_test_plugins", [None], indirect=True)
|
|
208
|
-
def test_load_plugins_should_refresh_event_protocol_map(
|
|
209
|
-
load_test_plugins: None, install_test_plugin: Path
|
|
210
|
-
) -> None:
|
|
211
|
-
"""Test that the event protocol map is refreshed when loading plugins."""
|
|
212
|
-
assert EVENT_HANDLER_MAP == {}
|
|
213
|
-
load_plugins()
|
|
214
|
-
assert EventType.Name(EventType.UNKNOWN) in EVENT_HANDLER_MAP
|
|
215
|
-
assert EVENT_HANDLER_MAP[EventType.Name(EventType.UNKNOWN)] == [
|
|
216
|
-
"example_plugin:example_plugin.protocols.my_protocol:Protocol"
|
|
217
|
-
]
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
@pytest.mark.asyncio
|
|
221
|
-
@pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
|
|
222
|
-
async def test_handle_plugin_event_returns_expected_result(
|
|
223
|
-
install_test_plugin: Path, plugin_runner: PluginRunner, load_test_plugins: None
|
|
224
|
-
) -> None:
|
|
225
|
-
"""Test that HandleEvent successfully calls the relevant plugins and returns the expected result."""
|
|
226
|
-
event = EventRequest(type=EventType.UNKNOWN)
|
|
227
|
-
|
|
228
|
-
result = []
|
|
229
|
-
async for response in plugin_runner.HandleEvent(event, None):
|
|
230
|
-
result.append(response)
|
|
231
|
-
|
|
232
|
-
assert len(result) == 1
|
|
233
|
-
assert result[0].success is True
|
|
234
|
-
assert len(result[0].effects) == 1
|
|
235
|
-
assert result[0].effects[0].type == EffectType.LOG
|
|
236
|
-
assert result[0].effects[0].payload == "Hello, world!"
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
@pytest.mark.asyncio
|
|
240
|
-
async def test_reload_plugins_event_handler_successfully_publishes_message(
|
|
241
|
-
plugin_runner: PluginRunner,
|
|
242
|
-
) -> None:
|
|
243
|
-
"""Test ReloadPlugins Event handler successfully publishes a message with restart action."""
|
|
244
|
-
with patch(
|
|
245
|
-
"plugin_runner.plugin_runner.publish_message", new_callable=AsyncMock
|
|
246
|
-
) as mock_publish_message:
|
|
247
|
-
request = ReloadPluginsRequest()
|
|
248
|
-
|
|
249
|
-
result = []
|
|
250
|
-
async for response in plugin_runner.ReloadPlugins(request, None):
|
|
251
|
-
result.append(response)
|
|
252
|
-
|
|
253
|
-
mock_publish_message.assert_called_once_with(message={"action": "reload"})
|
|
254
|
-
|
|
255
|
-
assert len(result) == 1
|
|
256
|
-
assert result[0].success is True
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
@pytest.mark.asyncio
|
|
260
|
-
async def test_synchronize_plugins_calls_install_and_load_plugins() -> None:
|
|
261
|
-
"""Test that synchronize_plugins calls install_plugins and load_plugins."""
|
|
262
|
-
with (
|
|
263
|
-
patch("plugin_runner.plugin_runner.get_client", new_callable=MagicMock) as mock_get_client,
|
|
264
|
-
patch(
|
|
265
|
-
"plugin_runner.plugin_runner.install_plugins", new_callable=AsyncMock
|
|
266
|
-
) as mock_install_plugins,
|
|
267
|
-
patch(
|
|
268
|
-
"plugin_runner.plugin_runner.load_plugins", new_callable=AsyncMock
|
|
269
|
-
) as mock_load_plugins,
|
|
270
|
-
):
|
|
271
|
-
mock_client = AsyncMock()
|
|
272
|
-
mock_pubsub = AsyncMock()
|
|
273
|
-
mock_get_client.return_value = (mock_client, mock_pubsub)
|
|
274
|
-
mock_pubsub.get_message.return_value = {
|
|
275
|
-
"type": "pmessage",
|
|
276
|
-
"data": pickle.dumps({"action": "reload"}),
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
task = asyncio.create_task(synchronize_plugins(run_once=True))
|
|
280
|
-
await asyncio.sleep(0.1) # Give some time for the coroutine to run
|
|
281
|
-
task.cancel()
|
|
282
|
-
|
|
283
|
-
mock_install_plugins.assert_called_once()
|
|
284
|
-
mock_load_plugins.assert_called_once()
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
@pytest.mark.asyncio
|
|
288
|
-
@pytest.mark.parametrize("install_test_plugin", ["test_module_imports_plugin"], indirect=True)
|
|
289
|
-
async def test_changes_to_plugin_modules_should_be_reflected_after_reload(
|
|
290
|
-
install_test_plugin: Path, load_test_plugins: None, plugin_runner: PluginRunner
|
|
291
|
-
) -> None:
|
|
292
|
-
"""Test that changes to plugin modules are reflected after reloading the plugin."""
|
|
293
|
-
event = EventRequest(type=EventType.UNKNOWN)
|
|
294
|
-
|
|
295
|
-
result = []
|
|
296
|
-
async for response in plugin_runner.HandleEvent(event, None):
|
|
297
|
-
result.append(response)
|
|
298
|
-
|
|
299
|
-
assert len(result) == 1
|
|
300
|
-
assert result[0].success is True
|
|
301
|
-
assert len(result[0].effects) == 1
|
|
302
|
-
assert result[0].effects[0].type == EffectType.LOG
|
|
303
|
-
assert result[0].effects[0].payload == "Successfully imported!"
|
|
304
|
-
|
|
305
|
-
NEW_CODE = """
|
|
306
|
-
def import_me() -> str:
|
|
307
|
-
return "Successfully changed!"
|
|
308
|
-
"""
|
|
309
|
-
file_path = install_test_plugin / "other_module" / "base.py"
|
|
310
|
-
file_path.write_text(NEW_CODE, encoding="utf-8")
|
|
311
|
-
|
|
312
|
-
# Reload the plugin
|
|
313
|
-
load_plugins()
|
|
314
|
-
|
|
315
|
-
result = []
|
|
316
|
-
async for response in plugin_runner.HandleEvent(event, None):
|
|
317
|
-
result.append(response)
|
|
318
|
-
|
|
319
|
-
assert len(result) == 1
|
|
320
|
-
assert result[0].success is True
|
|
321
|
-
assert len(result[0].effects) == 1
|
|
322
|
-
assert result[0].effects[0].type == EffectType.LOG
|
|
323
|
-
assert result[0].effects[0].payload == "Successfully changed!"
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
@pytest.mark.asyncio
|
|
327
|
-
@pytest.mark.parametrize(
|
|
328
|
-
argnames="context,status_code",
|
|
329
|
-
argvalues=[
|
|
330
|
-
(
|
|
331
|
-
{
|
|
332
|
-
"plugin_name": "test_simple_api",
|
|
333
|
-
"method": "GET",
|
|
334
|
-
"path": "/route",
|
|
335
|
-
"query_string": "",
|
|
336
|
-
"body": b64encode(b"").decode(),
|
|
337
|
-
"headers": {},
|
|
338
|
-
},
|
|
339
|
-
HTTPStatus.OK,
|
|
340
|
-
),
|
|
341
|
-
(
|
|
342
|
-
{
|
|
343
|
-
"plugin_name": "test_simple_api",
|
|
344
|
-
"method": "GET",
|
|
345
|
-
"path": "/notfound",
|
|
346
|
-
"query_string": "",
|
|
347
|
-
"body": b64encode(b"").decode(),
|
|
348
|
-
"headers": {},
|
|
349
|
-
},
|
|
350
|
-
HTTPStatus.NOT_FOUND,
|
|
351
|
-
),
|
|
352
|
-
(
|
|
353
|
-
{
|
|
354
|
-
"plugin_name": "test_simple_api",
|
|
355
|
-
"method": "GET",
|
|
356
|
-
"path": "/error",
|
|
357
|
-
"query_string": "",
|
|
358
|
-
"body": b64encode(b"").decode(),
|
|
359
|
-
"headers": {},
|
|
360
|
-
},
|
|
361
|
-
HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
362
|
-
),
|
|
363
|
-
],
|
|
364
|
-
ids=["success", "not found error", "multiple handlers error"],
|
|
365
|
-
)
|
|
366
|
-
@pytest.mark.parametrize("install_test_plugin", ["test_simple_api"], indirect=True)
|
|
367
|
-
async def test_simple_api(
|
|
368
|
-
install_test_plugin: Path,
|
|
369
|
-
load_test_plugins: None,
|
|
370
|
-
plugin_runner: PluginRunner,
|
|
371
|
-
context: dict[str, Any],
|
|
372
|
-
status_code: HTTPStatus,
|
|
373
|
-
) -> None:
|
|
374
|
-
"""Test that the PluginRunner returns responses to SimpleAPI request events."""
|
|
375
|
-
event = EventRequest(
|
|
376
|
-
type=EventType.SIMPLE_API_REQUEST,
|
|
377
|
-
context=json.dumps(context),
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
result = []
|
|
381
|
-
async for response in plugin_runner.HandleEvent(event, None):
|
|
382
|
-
result.append(response)
|
|
383
|
-
|
|
384
|
-
expected_response = Response(status_code=status_code).apply()
|
|
385
|
-
if status_code == HTTPStatus.OK:
|
|
386
|
-
expected_response.plugin_name = "test_simple_api"
|
|
387
|
-
|
|
388
|
-
assert result[0].effects == [expected_response]
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from plugin_runner.sandbox import FORBIDDEN_ASSIGNMENTS, Sandbox
|
|
4
|
-
|
|
5
|
-
# Sample code strings for testing various scenarios
|
|
6
|
-
VALID_CODE = """
|
|
7
|
-
x = 10
|
|
8
|
-
y = 20
|
|
9
|
-
result = x + y
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
CODE_WITH_RESTRICTED_IMPORT = """
|
|
13
|
-
import os
|
|
14
|
-
result = os.listdir('.')
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
CODE_WITH_PLUGIN_RUNNER_SETTING_IMPORT = """
|
|
18
|
-
import settings
|
|
19
|
-
result = settings.AWS_SECRET_ACCESS_KEY
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
CODE_WITH_ALLOWED_IMPORT = """
|
|
23
|
-
import json
|
|
24
|
-
result = json.dumps({"key": "value"})
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
CODE_WITH_FORBIDDEN_FUNC_NAME = """
|
|
28
|
-
builtins = {}
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
SOURCE_CODE_MODULE = """
|
|
32
|
-
import module.b
|
|
33
|
-
result = module.b
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
CODE_WITH_FORBIDDEN_ASSIGNMENTS = [
|
|
37
|
-
code
|
|
38
|
-
for var in FORBIDDEN_ASSIGNMENTS
|
|
39
|
-
for code in [
|
|
40
|
-
f"{var} = 'test'",
|
|
41
|
-
f"test = {var} = 'test'",
|
|
42
|
-
f"test = {var} = test2 = 'test'",
|
|
43
|
-
f"(a, (b, c), (d, ({var}, f))) = (1, (2, 3), (4, (5, 6)))",
|
|
44
|
-
f"(a, (b, c), (d, [{var}, f])) = (1, (2, 3), (4, [5, 6]))",
|
|
45
|
-
]
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def test_valid_code_execution() -> None:
|
|
50
|
-
"""Test execution of valid code in the sandbox."""
|
|
51
|
-
sandbox = Sandbox(VALID_CODE)
|
|
52
|
-
scope = sandbox.execute()
|
|
53
|
-
assert scope["result"] == 30, "The code should compute result as 30."
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def test_disallowed_import() -> None:
|
|
57
|
-
"""Test that restricted imports are not allowed."""
|
|
58
|
-
sandbox = Sandbox(CODE_WITH_RESTRICTED_IMPORT)
|
|
59
|
-
with pytest.raises(ImportError, match="'os' is not an allowed import."):
|
|
60
|
-
sandbox.execute()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_plugin_runner_settings_import() -> None:
|
|
64
|
-
"""Test that imports of plugin runner settings are not allowed."""
|
|
65
|
-
sandbox = Sandbox(CODE_WITH_PLUGIN_RUNNER_SETTING_IMPORT)
|
|
66
|
-
with pytest.raises(ImportError, match="'settings' is not an allowed import."):
|
|
67
|
-
sandbox.execute()
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def test_allowed_import() -> None:
|
|
71
|
-
"""Test that allowed imports (from ALLOWED_MODULES) work correctly."""
|
|
72
|
-
sandbox = Sandbox(CODE_WITH_ALLOWED_IMPORT)
|
|
73
|
-
scope = sandbox.execute()
|
|
74
|
-
assert scope["result"] == '{"key": "value"}', "JSON encoding should work with allowed imports."
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def test_forbidden_name() -> None:
|
|
78
|
-
"""Test that forbidden function names are blocked by Transformer."""
|
|
79
|
-
sandbox = Sandbox(CODE_WITH_FORBIDDEN_FUNC_NAME)
|
|
80
|
-
with pytest.raises(RuntimeError, match="Code is invalid"):
|
|
81
|
-
sandbox.execute()
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@pytest.mark.parametrize("code", CODE_WITH_FORBIDDEN_ASSIGNMENTS)
|
|
85
|
-
def test_forbidden_assignment(code: str) -> None:
|
|
86
|
-
"""Test that forbidden assignments are blocked by Transformer."""
|
|
87
|
-
sandbox = Sandbox(code)
|
|
88
|
-
with pytest.raises(RuntimeError, match="Code is invalid"):
|
|
89
|
-
sandbox.execute()
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def test_code_with_warnings() -> None:
|
|
93
|
-
"""Test that the sandbox captures warnings for restricted names or usage."""
|
|
94
|
-
code_with_warning = """
|
|
95
|
-
_x = 5
|
|
96
|
-
result = _x
|
|
97
|
-
"""
|
|
98
|
-
sandbox = Sandbox(code_with_warning)
|
|
99
|
-
assert sandbox.warnings, "There should be warnings for using restricted names."
|
|
100
|
-
scope = sandbox.execute()
|
|
101
|
-
assert scope["result"] == 5, "Code should execute despite warnings."
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def test_compile_errors() -> None:
|
|
105
|
-
"""Test that compile errors are detected for invalid syntax."""
|
|
106
|
-
invalid_code = """
|
|
107
|
-
def missing_colon()
|
|
108
|
-
return 42
|
|
109
|
-
"""
|
|
110
|
-
sandbox = Sandbox(invalid_code)
|
|
111
|
-
with pytest.raises(RuntimeError, match="Code is invalid"):
|
|
112
|
-
sandbox.execute()
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def test_sandbox_scope() -> None:
|
|
116
|
-
"""Verify the sandbox scope includes expected built-ins and utility functions."""
|
|
117
|
-
sandbox = Sandbox(VALID_CODE)
|
|
118
|
-
scope = sandbox.execute()
|
|
119
|
-
assert "any" in scope["__builtins__"]
|
|
120
|
-
assert scope["__builtins__"]["any"] == any, "'any' function should be accessible in sandbox."
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def test_print_collector() -> None:
|
|
124
|
-
"""Ensure that PrintCollector is used for capturing prints."""
|
|
125
|
-
code_with_print = """
|
|
126
|
-
print("Hello, Sandbox!")
|
|
127
|
-
"""
|
|
128
|
-
sandbox = Sandbox(code_with_print)
|
|
129
|
-
scope = sandbox.execute()
|
|
130
|
-
assert "Hello, Sandbox!" in scope["_print"].txt, "Print output should be captured."
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def test_sandbox_denies_module_name_import_outside_package() -> None:
|
|
134
|
-
"""Test that modules outside the root package cannot be imported."""
|
|
135
|
-
sandbox_module_a = Sandbox(source_code=SOURCE_CODE_MODULE, namespace="other_module.a")
|
|
136
|
-
with pytest.raises(ImportError, match="module.b' is not an allowed import."):
|
|
137
|
-
sandbox_module_a.execute()
|
|
File without changes
|
|
File without changes
|