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,1167 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import re
|
|
3
|
-
from base64 import b64decode, b64encode
|
|
4
|
-
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
5
|
-
from http import HTTPStatus
|
|
6
|
-
from types import SimpleNamespace
|
|
7
|
-
from typing import Any, TypeVar
|
|
8
|
-
from uuid import uuid4
|
|
9
|
-
|
|
10
|
-
import pytest
|
|
11
|
-
from _pytest.fixtures import SubRequest
|
|
12
|
-
|
|
13
|
-
from canvas_sdk.effects.simple_api import (
|
|
14
|
-
Effect,
|
|
15
|
-
EffectType,
|
|
16
|
-
HTMLResponse,
|
|
17
|
-
JSONResponse,
|
|
18
|
-
PlainTextResponse,
|
|
19
|
-
Response,
|
|
20
|
-
)
|
|
21
|
-
from canvas_sdk.events import Event, EventRequest, EventType
|
|
22
|
-
from canvas_sdk.handlers.simple_api import api
|
|
23
|
-
from canvas_sdk.handlers.simple_api.api import (
|
|
24
|
-
FileFormPart,
|
|
25
|
-
FormPart,
|
|
26
|
-
Request,
|
|
27
|
-
SimpleAPI,
|
|
28
|
-
SimpleAPIBase,
|
|
29
|
-
SimpleAPIRoute,
|
|
30
|
-
StringFormPart,
|
|
31
|
-
)
|
|
32
|
-
from canvas_sdk.handlers.simple_api.security import (
|
|
33
|
-
APIKeyAuthMixin,
|
|
34
|
-
APIKeyCredentials,
|
|
35
|
-
AuthSchemeMixin,
|
|
36
|
-
BasicAuthMixin,
|
|
37
|
-
BasicCredentials,
|
|
38
|
-
BearerCredentials,
|
|
39
|
-
Credentials,
|
|
40
|
-
PatientSessionAuthMixin,
|
|
41
|
-
SessionCredentials,
|
|
42
|
-
StaffSessionAuthMixin,
|
|
43
|
-
)
|
|
44
|
-
from canvas_sdk.handlers.simple_api.tools import (
|
|
45
|
-
CaseInsensitiveMultiDict,
|
|
46
|
-
MultiDict,
|
|
47
|
-
separate_headers,
|
|
48
|
-
)
|
|
49
|
-
from plugin_runner.exceptions import PluginError
|
|
50
|
-
|
|
51
|
-
REQUEST_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
|
52
|
-
HEADERS_RAW = {
|
|
53
|
-
"Canvas-Plugins-Test-Header-1": "test header 1",
|
|
54
|
-
"Canvas-Plugins-Test-Header-2": "test header 2a, test header 2b",
|
|
55
|
-
}
|
|
56
|
-
HEADERS = CaseInsensitiveMultiDict(separate_headers(HEADERS_RAW))
|
|
57
|
-
|
|
58
|
-
FORM = b64decode(
|
|
59
|
-
"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"
|
|
60
|
-
)
|
|
61
|
-
FILE = b64decode(
|
|
62
|
-
"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"
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
class NoAuthMixin:
|
|
67
|
-
"""Mixin to bypass authentication for tests that are not related to authentication."""
|
|
68
|
-
|
|
69
|
-
def authenticate(self, credentials: Credentials) -> bool:
|
|
70
|
-
"""Authenticate the request."""
|
|
71
|
-
return True
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class RouteNoAuth(NoAuthMixin, SimpleAPIRoute):
|
|
75
|
-
"""Route class that bypasses authentication."""
|
|
76
|
-
|
|
77
|
-
pass
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
class APINoAuth(NoAuthMixin, SimpleAPI):
|
|
81
|
-
"""API class that bypasses authentication."""
|
|
82
|
-
|
|
83
|
-
pass
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def make_event(
|
|
87
|
-
event_type: EventType,
|
|
88
|
-
method: str,
|
|
89
|
-
path: str,
|
|
90
|
-
query_string: str | None = None,
|
|
91
|
-
body: bytes | None = None,
|
|
92
|
-
headers: Mapping[str, str] | None = None,
|
|
93
|
-
) -> Event:
|
|
94
|
-
"""Make a SIMPLE_API_REQUEST event suitable for testing."""
|
|
95
|
-
if event_type == EventType.SIMPLE_API_AUTHENTICATE:
|
|
96
|
-
body = b""
|
|
97
|
-
|
|
98
|
-
return Event(
|
|
99
|
-
event_request=EventRequest(
|
|
100
|
-
type=event_type,
|
|
101
|
-
target=None,
|
|
102
|
-
context=json.dumps(
|
|
103
|
-
{
|
|
104
|
-
"method": method,
|
|
105
|
-
"path": path,
|
|
106
|
-
"query_string": query_string or "",
|
|
107
|
-
"body": b64encode(body or b"").decode(),
|
|
108
|
-
"headers": dict(headers) if headers else {},
|
|
109
|
-
},
|
|
110
|
-
indent=None,
|
|
111
|
-
separators=(",", ":"),
|
|
112
|
-
),
|
|
113
|
-
target_type=None,
|
|
114
|
-
)
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def handle_request(
|
|
119
|
-
cls: type[SimpleAPIBase],
|
|
120
|
-
method: str,
|
|
121
|
-
path: str,
|
|
122
|
-
query_string: str | None = None,
|
|
123
|
-
body: bytes | None = None,
|
|
124
|
-
headers: Mapping[str, str] | None = None,
|
|
125
|
-
) -> list[Effect]:
|
|
126
|
-
"""
|
|
127
|
-
Mimic the two-pass request handling in home-app.
|
|
128
|
-
|
|
129
|
-
First, handle the authentication event, and if it succeeds, handle the request event.
|
|
130
|
-
"""
|
|
131
|
-
handler = cls(
|
|
132
|
-
make_event(EventType.SIMPLE_API_AUTHENTICATE, method, path, query_string, body, headers)
|
|
133
|
-
)
|
|
134
|
-
effects = handler.compute()
|
|
135
|
-
|
|
136
|
-
payload = json.loads(effects[0].payload)
|
|
137
|
-
if payload["status_code"] != HTTPStatus.OK:
|
|
138
|
-
return effects
|
|
139
|
-
|
|
140
|
-
handler = cls(
|
|
141
|
-
make_event(EventType.SIMPLE_API_REQUEST, method, path, query_string, body, headers)
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
return handler.compute()
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
T = TypeVar("T")
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
@pytest.mark.parametrize(
|
|
151
|
-
argnames="func,expected_value",
|
|
152
|
-
argvalues=[
|
|
153
|
-
(lambda m: m["b"], 2),
|
|
154
|
-
(lambda m: m["a"], 1),
|
|
155
|
-
(lambda m: len(m), 2),
|
|
156
|
-
(lambda m: next(iter(m.items())), ("a", 1)),
|
|
157
|
-
(lambda m: "a" in m, True),
|
|
158
|
-
(lambda m: "d" in m, False),
|
|
159
|
-
(lambda m: m.get("a"), 1),
|
|
160
|
-
(lambda m: m.get("d", 4), 4),
|
|
161
|
-
(lambda m: m.get("d"), None),
|
|
162
|
-
(lambda m: m.get_list("a"), [1, 3]),
|
|
163
|
-
(
|
|
164
|
-
lambda m: [(k, v) for k, v in m.items()],
|
|
165
|
-
[("a", 1), ("b", 2)],
|
|
166
|
-
),
|
|
167
|
-
(
|
|
168
|
-
lambda m: [(k, v) for k, v in m.multi_items()],
|
|
169
|
-
[("a", 1), ("b", 2), ("a", 3)],
|
|
170
|
-
),
|
|
171
|
-
(lambda m: list(m.keys()), ["a", "b"]),
|
|
172
|
-
(lambda m: list(reversed(m)), ["b", "a"]),
|
|
173
|
-
(lambda m: list(m.values()), [1, 2]),
|
|
174
|
-
(lambda m: m == MultiDict((("a", 1), ("b", 2), ("a", 3))), True),
|
|
175
|
-
(lambda m: m != MultiDict((("a", 1), ("b", 2))), True),
|
|
176
|
-
],
|
|
177
|
-
ids=[
|
|
178
|
-
"[] single value from single value",
|
|
179
|
-
"[] single value from multiple values",
|
|
180
|
-
"len",
|
|
181
|
-
"iter",
|
|
182
|
-
"in",
|
|
183
|
-
"not in",
|
|
184
|
-
"get",
|
|
185
|
-
"get default",
|
|
186
|
-
"get no default",
|
|
187
|
-
"get_list",
|
|
188
|
-
"items",
|
|
189
|
-
"multi_items",
|
|
190
|
-
"keys",
|
|
191
|
-
"reversed",
|
|
192
|
-
"values",
|
|
193
|
-
"==",
|
|
194
|
-
"!=",
|
|
195
|
-
],
|
|
196
|
-
)
|
|
197
|
-
def test_multidict(func: Callable[[MultiDict[str, int]], T], expected_value: T) -> None:
|
|
198
|
-
"""Test the methods and functionality of MultiDict."""
|
|
199
|
-
multidict = MultiDict((("a", 1), ("b", 2), ("a", 3)))
|
|
200
|
-
assert func(multidict) == expected_value
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
@pytest.mark.parametrize(
|
|
204
|
-
argnames="func,expected_value",
|
|
205
|
-
argvalues=[
|
|
206
|
-
(lambda m: m["b"] == m["B"] == 2, True),
|
|
207
|
-
(lambda m: "a" in m and "A" in m, True),
|
|
208
|
-
(lambda m: "d" not in m and "D" not in m, True),
|
|
209
|
-
(lambda m: m.get("a") == m.get("A") == 1, True),
|
|
210
|
-
(lambda m: m.get_list("a") == m.get_list("A") == [1, 3], True),
|
|
211
|
-
],
|
|
212
|
-
ids=["[]", "in", "not in", "get", "get_list"],
|
|
213
|
-
)
|
|
214
|
-
def test_case_insensitive_multidict(
|
|
215
|
-
func: Callable[[MultiDict[str, int]], T], expected_value: T
|
|
216
|
-
) -> None:
|
|
217
|
-
"""Test the methods and functionality of CaseInsensitiveMultiDict."""
|
|
218
|
-
multidict = CaseInsensitiveMultiDict((("a", 1), ("b", 2), ("A", 3)))
|
|
219
|
-
assert func(multidict) == expected_value
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
@pytest.mark.parametrize(
|
|
223
|
-
argnames="method,body,headers",
|
|
224
|
-
argvalues=[
|
|
225
|
-
("GET", b"", HEADERS_RAW),
|
|
226
|
-
(
|
|
227
|
-
"POST",
|
|
228
|
-
b'{"message": "JSON request"}',
|
|
229
|
-
{"Content-Type": "application/json"} | HEADERS_RAW,
|
|
230
|
-
),
|
|
231
|
-
(
|
|
232
|
-
"POST",
|
|
233
|
-
b"plain text request",
|
|
234
|
-
{"Content-Type": "text/plain"} | HEADERS_RAW,
|
|
235
|
-
),
|
|
236
|
-
("POST", b"<html></html>", {"Content-Type": "text/html"} | HEADERS_RAW),
|
|
237
|
-
],
|
|
238
|
-
ids=["no body", "JSON", "plain text", "HTML"],
|
|
239
|
-
)
|
|
240
|
-
def test_request(
|
|
241
|
-
method: str,
|
|
242
|
-
body: bytes,
|
|
243
|
-
headers: Mapping[str, str],
|
|
244
|
-
) -> None:
|
|
245
|
-
"""Test the construction of a Request object and access to its attributes."""
|
|
246
|
-
path = "/route"
|
|
247
|
-
query_string = "value1=a&value2=b"
|
|
248
|
-
request = Request(
|
|
249
|
-
make_event(EventType.SIMPLE_API_REQUEST, method, path, query_string, body, headers),
|
|
250
|
-
path_pattern=re.compile(path),
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
assert request.method == method
|
|
254
|
-
assert request.path == path
|
|
255
|
-
assert request.query_string == query_string
|
|
256
|
-
assert request.body == body
|
|
257
|
-
|
|
258
|
-
assert request.headers == CaseInsensitiveMultiDict(separate_headers(headers))
|
|
259
|
-
assert request.headers["canvas-plugins-test-header-1"] == "test header 1"
|
|
260
|
-
assert request.headers["canvas-plugins-test-header-2"] == "test header 2a"
|
|
261
|
-
assert request.headers.get_list("canvas-plugins-test-header-1") == ["test header 1"]
|
|
262
|
-
assert request.headers.get_list("canvas-plugins-test-header-2") == [
|
|
263
|
-
"test header 2a",
|
|
264
|
-
"test header 2b",
|
|
265
|
-
]
|
|
266
|
-
|
|
267
|
-
assert request.query_params == MultiDict((("value1", "a"), ("value2", "b")))
|
|
268
|
-
|
|
269
|
-
assert request.content_type == request.headers.get("content-type")
|
|
270
|
-
|
|
271
|
-
if request.content_type:
|
|
272
|
-
if request.content_type == "application/json":
|
|
273
|
-
assert request.json() == json.loads(body)
|
|
274
|
-
elif request.content_type.startswith("text/"):
|
|
275
|
-
assert request.text() == body.decode()
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
@pytest.mark.parametrize(
|
|
279
|
-
argnames="body,content_type,expected_form_data",
|
|
280
|
-
argvalues=[
|
|
281
|
-
(
|
|
282
|
-
b"part1=value1&part2=value2&part1=value3",
|
|
283
|
-
"application/x-www-form-urlencoded",
|
|
284
|
-
MultiDict(
|
|
285
|
-
(
|
|
286
|
-
("part1", StringFormPart(name="part1", value="value1")),
|
|
287
|
-
("part2", StringFormPart(name="part2", value="value2")),
|
|
288
|
-
("part1", StringFormPart(name="part1", value="value3")),
|
|
289
|
-
)
|
|
290
|
-
),
|
|
291
|
-
),
|
|
292
|
-
(
|
|
293
|
-
FORM,
|
|
294
|
-
"multipart/form-data; boundary=--------------------------966149001464621638881292",
|
|
295
|
-
MultiDict(
|
|
296
|
-
(
|
|
297
|
-
("part1", StringFormPart(name="part1", value="value1")),
|
|
298
|
-
("part2", StringFormPart(name="part2", value="value2")),
|
|
299
|
-
(
|
|
300
|
-
"part1",
|
|
301
|
-
FileFormPart(
|
|
302
|
-
name="part1",
|
|
303
|
-
filename="Sydney.jpg",
|
|
304
|
-
content=FILE,
|
|
305
|
-
content_type="image/jpeg",
|
|
306
|
-
),
|
|
307
|
-
),
|
|
308
|
-
)
|
|
309
|
-
),
|
|
310
|
-
),
|
|
311
|
-
],
|
|
312
|
-
ids=["x-www-form-urlencoded", "multipart/form-data"],
|
|
313
|
-
)
|
|
314
|
-
def test_request_form(
|
|
315
|
-
body: bytes, content_type: str, expected_form_data: Mapping[str, Sequence[FormPart]]
|
|
316
|
-
) -> None:
|
|
317
|
-
"""Test the parsing of form data from the request body."""
|
|
318
|
-
request = Request(
|
|
319
|
-
make_event(
|
|
320
|
-
EventType.SIMPLE_API_REQUEST,
|
|
321
|
-
method="POST",
|
|
322
|
-
path="/route",
|
|
323
|
-
body=body,
|
|
324
|
-
headers={"Content-Type": content_type},
|
|
325
|
-
),
|
|
326
|
-
path_pattern=re.compile("/route"),
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
assert request.form_data() == expected_form_data
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def response_body(effects: Iterable[Effect]) -> bytes:
|
|
333
|
-
"""Given a list of effects, find the response object and return the body."""
|
|
334
|
-
for effect in effects:
|
|
335
|
-
if effect.type == EffectType.SIMPLE_API_RESPONSE:
|
|
336
|
-
payload = json.loads(effect.payload)
|
|
337
|
-
return b64decode(payload["body"].encode())
|
|
338
|
-
|
|
339
|
-
pytest.fail("No response effect was found in the list of effects")
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
def json_response_body(effects: Iterable[Effect]) -> Any:
|
|
343
|
-
"""Given a list of effects, find the response object and return the JSON body."""
|
|
344
|
-
return json.loads(response_body(effects))
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
@pytest.mark.parametrize(argnames="method", argvalues=REQUEST_METHODS, ids=REQUEST_METHODS)
|
|
348
|
-
def test_request_routing_route(method: str) -> None:
|
|
349
|
-
"""Test request routing for SimpleAPIRoute plugins."""
|
|
350
|
-
|
|
351
|
-
class Route(RouteNoAuth):
|
|
352
|
-
PATH = "/route"
|
|
353
|
-
|
|
354
|
-
def get(self) -> list[Response | Effect]:
|
|
355
|
-
return [
|
|
356
|
-
JSONResponse(
|
|
357
|
-
{"method": "GET"},
|
|
358
|
-
)
|
|
359
|
-
]
|
|
360
|
-
|
|
361
|
-
def post(self) -> list[Response | Effect]:
|
|
362
|
-
return [
|
|
363
|
-
JSONResponse(
|
|
364
|
-
{"method": "POST"},
|
|
365
|
-
)
|
|
366
|
-
]
|
|
367
|
-
|
|
368
|
-
def put(self) -> list[Response | Effect]:
|
|
369
|
-
return [
|
|
370
|
-
JSONResponse(
|
|
371
|
-
{"method": "PUT"},
|
|
372
|
-
)
|
|
373
|
-
]
|
|
374
|
-
|
|
375
|
-
def delete(self) -> list[Response | Effect]:
|
|
376
|
-
return [
|
|
377
|
-
JSONResponse(
|
|
378
|
-
{"method": "DELETE"},
|
|
379
|
-
)
|
|
380
|
-
]
|
|
381
|
-
|
|
382
|
-
def patch(self) -> list[Response | Effect]:
|
|
383
|
-
return [
|
|
384
|
-
JSONResponse(
|
|
385
|
-
{"method": "PATCH"},
|
|
386
|
-
)
|
|
387
|
-
]
|
|
388
|
-
|
|
389
|
-
effects = handle_request(Route, method, path="/route")
|
|
390
|
-
body = json_response_body(effects)
|
|
391
|
-
|
|
392
|
-
assert body["method"] == method
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
@pytest.mark.parametrize(
|
|
396
|
-
argnames="path", argvalues=["/route1", "/route2"], ids=["route1", "route2"]
|
|
397
|
-
)
|
|
398
|
-
@pytest.mark.parametrize(
|
|
399
|
-
argnames="prefix",
|
|
400
|
-
argvalues=["/prefix", "", None],
|
|
401
|
-
ids=["with prefix", "empty prefix", "no prefix"],
|
|
402
|
-
)
|
|
403
|
-
@pytest.mark.parametrize(
|
|
404
|
-
argnames="decorator,method",
|
|
405
|
-
argvalues=[
|
|
406
|
-
(api.get, "GET"),
|
|
407
|
-
(api.post, "POST"),
|
|
408
|
-
(api.put, "PUT"),
|
|
409
|
-
(api.delete, "DELETE"),
|
|
410
|
-
(api.patch, "PATCH"),
|
|
411
|
-
],
|
|
412
|
-
ids=REQUEST_METHODS,
|
|
413
|
-
)
|
|
414
|
-
def test_request_routing_api(
|
|
415
|
-
decorator: Callable[[str], Callable], method: str, prefix: str | None, path: str
|
|
416
|
-
) -> None:
|
|
417
|
-
"""Test request routing for SimpleAPI plugins."""
|
|
418
|
-
|
|
419
|
-
class API(APINoAuth):
|
|
420
|
-
PREFIX = prefix
|
|
421
|
-
|
|
422
|
-
@decorator("/route1")
|
|
423
|
-
def route1(self) -> list[Response | Effect]:
|
|
424
|
-
return [
|
|
425
|
-
JSONResponse(
|
|
426
|
-
{"method": method},
|
|
427
|
-
)
|
|
428
|
-
]
|
|
429
|
-
|
|
430
|
-
@decorator("/route2")
|
|
431
|
-
def route2(self) -> list[Response | Effect]:
|
|
432
|
-
return [
|
|
433
|
-
JSONResponse(
|
|
434
|
-
{"method": method},
|
|
435
|
-
)
|
|
436
|
-
]
|
|
437
|
-
|
|
438
|
-
effects = handle_request(API, method, path=f"{prefix or ''}{path}")
|
|
439
|
-
body = json_response_body(effects)
|
|
440
|
-
|
|
441
|
-
assert body["method"] == method
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
@pytest.mark.parametrize(
|
|
445
|
-
argnames="prefix_pattern,path_pattern,body_func,path,expected_body",
|
|
446
|
-
argvalues=[
|
|
447
|
-
(
|
|
448
|
-
"/prefix",
|
|
449
|
-
"/path/<param>",
|
|
450
|
-
lambda params: {"param": params["param"]},
|
|
451
|
-
"/prefix/path/value",
|
|
452
|
-
{"param": "value"},
|
|
453
|
-
),
|
|
454
|
-
(
|
|
455
|
-
"/prefix",
|
|
456
|
-
"/path1/<param1>/path2/<param2>",
|
|
457
|
-
lambda params: {"param1": params["param1"], "param2": params["param2"]},
|
|
458
|
-
"/prefix/path1/value1/path2/value2",
|
|
459
|
-
{"param1": "value1", "param2": "value2"},
|
|
460
|
-
),
|
|
461
|
-
(
|
|
462
|
-
"/prefix/<param>",
|
|
463
|
-
"/path",
|
|
464
|
-
lambda params: {"param": params["param"]},
|
|
465
|
-
"/prefix/value/path",
|
|
466
|
-
{"param": "value"},
|
|
467
|
-
),
|
|
468
|
-
(
|
|
469
|
-
"/<param1>/prefix/<param2>",
|
|
470
|
-
"/path",
|
|
471
|
-
lambda params: {"param1": params["param1"], "param2": params["param2"]},
|
|
472
|
-
"/value1/prefix/value2/path",
|
|
473
|
-
{"param1": "value1", "param2": "value2"},
|
|
474
|
-
),
|
|
475
|
-
(
|
|
476
|
-
"/prefix/<param1>",
|
|
477
|
-
"/path/<param2>",
|
|
478
|
-
lambda params: {"param1": params["param1"], "param2": params["param2"]},
|
|
479
|
-
"/prefix/value1/path/value2",
|
|
480
|
-
{"param1": "value1", "param2": "value2"},
|
|
481
|
-
),
|
|
482
|
-
],
|
|
483
|
-
ids=[
|
|
484
|
-
"single parameter in path",
|
|
485
|
-
"multiple parameters in path",
|
|
486
|
-
"single parameter in prefix",
|
|
487
|
-
"multiple parameters in prefix",
|
|
488
|
-
"parameters in path and prefix",
|
|
489
|
-
],
|
|
490
|
-
)
|
|
491
|
-
def test_request_routing_path_pattern(
|
|
492
|
-
prefix_pattern: str,
|
|
493
|
-
path_pattern: str,
|
|
494
|
-
body_func: Callable[[Mapping[str, str]], Mapping[str, str]],
|
|
495
|
-
path: str,
|
|
496
|
-
expected_body: Mapping[str, str],
|
|
497
|
-
) -> None:
|
|
498
|
-
"""Test Request routing for routes that use path patterns."""
|
|
499
|
-
|
|
500
|
-
class API(APINoAuth):
|
|
501
|
-
PREFIX = prefix_pattern
|
|
502
|
-
|
|
503
|
-
@api.get(path_pattern)
|
|
504
|
-
def route(self) -> list[Response | Effect]:
|
|
505
|
-
return [JSONResponse(body_func(self.request.path_params))]
|
|
506
|
-
|
|
507
|
-
effects = handle_request(API, "GET", path=path)
|
|
508
|
-
body = json_response_body(effects)
|
|
509
|
-
|
|
510
|
-
assert body == expected_body
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
@pytest.mark.parametrize(
|
|
514
|
-
argnames="path_pattern1,path_pattern2,path,expected_body",
|
|
515
|
-
argvalues=[
|
|
516
|
-
("/path/<value>", "/path/test", "/prefix/path/value", {"handler_method": "first"}),
|
|
517
|
-
("/path/<value>", "/path/test", "/prefix/path/test", {"handler_method": "first"}),
|
|
518
|
-
("/path/test", "/path/<value>", "/prefix/path/test", {"handler_method": "first"}),
|
|
519
|
-
("/path/test", "/path/<value>", "/prefix/path/value", {"handler_method": "second"}),
|
|
520
|
-
(
|
|
521
|
-
"/path/<value>",
|
|
522
|
-
"/path/<value>/test",
|
|
523
|
-
"/prefix/path/value/test",
|
|
524
|
-
{"handler_method": "second"},
|
|
525
|
-
),
|
|
526
|
-
],
|
|
527
|
-
ids=[
|
|
528
|
-
"pattern registered first, path matches only pattern",
|
|
529
|
-
"pattern registered first, path matches both pattern and fixed",
|
|
530
|
-
"fixed registered first, path matches both pattern and fixed",
|
|
531
|
-
"fixed registered first, path matches only pattern",
|
|
532
|
-
"two patterns share the same first two segments and then diverge",
|
|
533
|
-
],
|
|
534
|
-
)
|
|
535
|
-
def test_request_routing_path_pattern_multiple_matches(
|
|
536
|
-
path_pattern1: str, path_pattern2: str, path: str, expected_body: Mapping[str, str]
|
|
537
|
-
) -> None:
|
|
538
|
-
"""Test request routing for path patterns where a path matches multiple routes in a handler."""
|
|
539
|
-
|
|
540
|
-
class API(APINoAuth):
|
|
541
|
-
PREFIX = "/prefix"
|
|
542
|
-
|
|
543
|
-
@api.get(path_pattern1)
|
|
544
|
-
def route1(self) -> list[Response | Effect]:
|
|
545
|
-
return [JSONResponse({"handler_method": "first"})]
|
|
546
|
-
|
|
547
|
-
@api.get(path_pattern2)
|
|
548
|
-
def route2(self) -> list[Response | Effect]:
|
|
549
|
-
return [JSONResponse({"handler_method": "second"})]
|
|
550
|
-
|
|
551
|
-
effects = handle_request(API, "GET", path=path)
|
|
552
|
-
body = json_response_body(effects)
|
|
553
|
-
|
|
554
|
-
assert body == expected_body
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
def test_request_lifecycle() -> None:
|
|
558
|
-
"""Test the request-response lifecycle."""
|
|
559
|
-
|
|
560
|
-
class Route(RouteNoAuth):
|
|
561
|
-
PATH = "/route"
|
|
562
|
-
|
|
563
|
-
def post(self) -> list[Response | Effect]:
|
|
564
|
-
return [
|
|
565
|
-
JSONResponse(
|
|
566
|
-
{
|
|
567
|
-
"method": self.request.method,
|
|
568
|
-
"path": self.request.path,
|
|
569
|
-
"query_string": self.request.query_string,
|
|
570
|
-
"body": self.request.json(),
|
|
571
|
-
"headers": dict(self.request.headers),
|
|
572
|
-
},
|
|
573
|
-
)
|
|
574
|
-
]
|
|
575
|
-
|
|
576
|
-
effects = handle_request(
|
|
577
|
-
Route,
|
|
578
|
-
method="POST",
|
|
579
|
-
path="/route",
|
|
580
|
-
query_string="value1=a&value2=b",
|
|
581
|
-
body=b'{"message": "JSON request"}',
|
|
582
|
-
headers=HEADERS,
|
|
583
|
-
)
|
|
584
|
-
body = json_response_body(effects)
|
|
585
|
-
|
|
586
|
-
assert body == {
|
|
587
|
-
"body": {"message": "JSON request"},
|
|
588
|
-
"headers": {k.lower(): v for k, v in HEADERS.items()},
|
|
589
|
-
"method": "POST",
|
|
590
|
-
"path": "/route",
|
|
591
|
-
"query_string": "value1=a&value2=b",
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
@pytest.mark.parametrize(
|
|
596
|
-
argnames="response,expected_effects",
|
|
597
|
-
argvalues=[
|
|
598
|
-
(
|
|
599
|
-
lambda: [
|
|
600
|
-
Effect(type=EffectType.CREATE_TASK, payload="create task"),
|
|
601
|
-
Effect(type=EffectType.ADD_BANNER_ALERT, payload="add banner alert"),
|
|
602
|
-
],
|
|
603
|
-
[
|
|
604
|
-
Effect(type=EffectType.CREATE_TASK, payload="create task"),
|
|
605
|
-
Effect(type=EffectType.ADD_BANNER_ALERT, payload="add banner alert"),
|
|
606
|
-
],
|
|
607
|
-
),
|
|
608
|
-
(
|
|
609
|
-
lambda: [
|
|
610
|
-
JSONResponse(
|
|
611
|
-
content={"message": "JSON response"},
|
|
612
|
-
status_code=HTTPStatus.ACCEPTED,
|
|
613
|
-
headers=HEADERS,
|
|
614
|
-
),
|
|
615
|
-
Effect(type=EffectType.CREATE_TASK, payload="create task"),
|
|
616
|
-
],
|
|
617
|
-
[
|
|
618
|
-
JSONResponse(
|
|
619
|
-
content={"message": "JSON response"},
|
|
620
|
-
status_code=HTTPStatus.ACCEPTED,
|
|
621
|
-
headers=HEADERS,
|
|
622
|
-
).apply(),
|
|
623
|
-
Effect(type=EffectType.CREATE_TASK, payload="create task"),
|
|
624
|
-
],
|
|
625
|
-
),
|
|
626
|
-
(
|
|
627
|
-
lambda: [
|
|
628
|
-
JSONResponse(
|
|
629
|
-
content={"message": "JSON response"},
|
|
630
|
-
status_code=HTTPStatus.ACCEPTED,
|
|
631
|
-
headers=HEADERS,
|
|
632
|
-
).apply(),
|
|
633
|
-
Effect(type=EffectType.CREATE_TASK, payload="create task"),
|
|
634
|
-
],
|
|
635
|
-
[
|
|
636
|
-
JSONResponse(
|
|
637
|
-
content={"message": "JSON response"},
|
|
638
|
-
status_code=HTTPStatus.ACCEPTED,
|
|
639
|
-
headers=HEADERS,
|
|
640
|
-
).apply(),
|
|
641
|
-
Effect(type=EffectType.CREATE_TASK, payload="create task"),
|
|
642
|
-
],
|
|
643
|
-
),
|
|
644
|
-
(lambda: [], []),
|
|
645
|
-
(
|
|
646
|
-
lambda: [Response(), Response()],
|
|
647
|
-
[Response(status_code=HTTPStatus.INTERNAL_SERVER_ERROR).apply()],
|
|
648
|
-
),
|
|
649
|
-
(
|
|
650
|
-
lambda: [
|
|
651
|
-
JSONResponse(
|
|
652
|
-
content={"message": "JSON response"},
|
|
653
|
-
status_code=HTTPStatus.BAD_REQUEST,
|
|
654
|
-
headers=HEADERS,
|
|
655
|
-
),
|
|
656
|
-
Effect(type=EffectType.CREATE_TASK, payload="create task"),
|
|
657
|
-
],
|
|
658
|
-
[
|
|
659
|
-
JSONResponse(
|
|
660
|
-
content={"message": "JSON response"},
|
|
661
|
-
status_code=HTTPStatus.BAD_REQUEST,
|
|
662
|
-
headers=HEADERS,
|
|
663
|
-
).apply()
|
|
664
|
-
],
|
|
665
|
-
),
|
|
666
|
-
(
|
|
667
|
-
lambda: [
|
|
668
|
-
JSONResponse(
|
|
669
|
-
content={"message": "JSON response"},
|
|
670
|
-
status_code=HTTPStatus.BAD_REQUEST,
|
|
671
|
-
headers=HEADERS,
|
|
672
|
-
).apply(),
|
|
673
|
-
Effect(type=EffectType.CREATE_TASK, payload="create task"),
|
|
674
|
-
],
|
|
675
|
-
[
|
|
676
|
-
JSONResponse(
|
|
677
|
-
content={"message": "JSON response"},
|
|
678
|
-
status_code=HTTPStatus.BAD_REQUEST,
|
|
679
|
-
headers=HEADERS,
|
|
680
|
-
).apply()
|
|
681
|
-
],
|
|
682
|
-
),
|
|
683
|
-
(
|
|
684
|
-
lambda: [
|
|
685
|
-
JSONResponse(content={"message": 1 / 0}, status_code=HTTPStatus.OK, headers=HEADERS)
|
|
686
|
-
],
|
|
687
|
-
[Response(status_code=HTTPStatus.INTERNAL_SERVER_ERROR).apply()],
|
|
688
|
-
),
|
|
689
|
-
],
|
|
690
|
-
ids=[
|
|
691
|
-
"list of effects",
|
|
692
|
-
"list of effects with response object",
|
|
693
|
-
"list of effects with response effect",
|
|
694
|
-
"no response",
|
|
695
|
-
"multiple responses",
|
|
696
|
-
"handler returns error response object",
|
|
697
|
-
"handler returns error response effect",
|
|
698
|
-
"exception in handler",
|
|
699
|
-
],
|
|
700
|
-
)
|
|
701
|
-
def test_response(response: Callable, expected_effects: Sequence[Effect]) -> None:
|
|
702
|
-
"""Test the construction and return of different kinds of responses."""
|
|
703
|
-
|
|
704
|
-
class Route(RouteNoAuth):
|
|
705
|
-
PATH = "/route"
|
|
706
|
-
|
|
707
|
-
def get(self) -> list[Response | Effect]:
|
|
708
|
-
return response()
|
|
709
|
-
|
|
710
|
-
effects = handle_request(Route, method="GET", path="/route")
|
|
711
|
-
|
|
712
|
-
assert effects == expected_effects
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
@pytest.mark.parametrize(
|
|
716
|
-
argnames="response,expected_payload",
|
|
717
|
-
argvalues=[
|
|
718
|
-
(
|
|
719
|
-
Response(
|
|
720
|
-
content=b"%PDF-1.4\n%\xd3\xeb\xe9\xe1",
|
|
721
|
-
status_code=HTTPStatus.ACCEPTED,
|
|
722
|
-
headers=HEADERS,
|
|
723
|
-
content_type="application/pdf",
|
|
724
|
-
),
|
|
725
|
-
'{"headers": {"canvas-plugins-test-header-1": "test header 1", '
|
|
726
|
-
'"canvas-plugins-test-header-2": "test header 2a", "Content-Type": "application/pdf"}, '
|
|
727
|
-
'"body": "JVBERi0xLjQKJdPr6eE=", "status_code": 202}',
|
|
728
|
-
),
|
|
729
|
-
(
|
|
730
|
-
JSONResponse(
|
|
731
|
-
content={"message": "JSON response"},
|
|
732
|
-
status_code=HTTPStatus.ACCEPTED,
|
|
733
|
-
headers=HEADERS,
|
|
734
|
-
),
|
|
735
|
-
'{"headers": {"canvas-plugins-test-header-1": "test header 1", '
|
|
736
|
-
'"canvas-plugins-test-header-2": "test header 2a", "Content-Type": "application/json"},'
|
|
737
|
-
' "body": "eyJtZXNzYWdlIjogIkpTT04gcmVzcG9uc2UifQ==", "status_code": 202}',
|
|
738
|
-
),
|
|
739
|
-
(
|
|
740
|
-
PlainTextResponse(
|
|
741
|
-
content="plain text response", status_code=HTTPStatus.ACCEPTED, headers=HEADERS
|
|
742
|
-
),
|
|
743
|
-
'{"headers": {"canvas-plugins-test-header-1": "test header 1", '
|
|
744
|
-
'"canvas-plugins-test-header-2": "test header 2a", "Content-Type": "text/plain"}, '
|
|
745
|
-
'"body": "cGxhaW4gdGV4dCByZXNwb25zZQ==", "status_code": 202}',
|
|
746
|
-
),
|
|
747
|
-
(
|
|
748
|
-
HTMLResponse(content="<html></html>", status_code=HTTPStatus.ACCEPTED, headers=HEADERS),
|
|
749
|
-
'{"headers": {"canvas-plugins-test-header-1": "test header 1", '
|
|
750
|
-
'"canvas-plugins-test-header-2": "test header 2a", "Content-Type": "text/html"}, '
|
|
751
|
-
'"body": "PGh0bWw+PC9odG1sPg==", "status_code": 202}',
|
|
752
|
-
),
|
|
753
|
-
(
|
|
754
|
-
Response(status_code=HTTPStatus.NO_CONTENT, headers=HEADERS),
|
|
755
|
-
'{"headers": {"canvas-plugins-test-header-1": "test header 1", '
|
|
756
|
-
'"canvas-plugins-test-header-2": "test header 2a"}, "body": "", "status_code": 204}',
|
|
757
|
-
),
|
|
758
|
-
],
|
|
759
|
-
ids=["binary", "JSON", "plain text", "HTML", "no content"],
|
|
760
|
-
)
|
|
761
|
-
def test_response_type(response: Response, expected_payload: str) -> None:
|
|
762
|
-
"""Test the Response object with different types of content."""
|
|
763
|
-
assert response.apply() == Effect(type=EffectType.SIMPLE_API_RESPONSE, payload=expected_payload)
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
def test_override_base_handler_attributes_error() -> None:
|
|
767
|
-
"""Test the enforcement of the error that occurs when base handler attributes are overridden."""
|
|
768
|
-
with pytest.raises(PluginError):
|
|
769
|
-
|
|
770
|
-
class API(APINoAuth):
|
|
771
|
-
@api.get("/route")
|
|
772
|
-
def compute(self) -> list[Response | Effect]: # type: ignore[override]
|
|
773
|
-
return []
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
def test_multiple_handlers_for_route_error() -> None:
|
|
777
|
-
"""
|
|
778
|
-
Test the enforcement of the error that occurs when a route is assigned to multiple handlers.
|
|
779
|
-
"""
|
|
780
|
-
with pytest.raises(PluginError):
|
|
781
|
-
|
|
782
|
-
class API(APINoAuth):
|
|
783
|
-
@api.get("/route")
|
|
784
|
-
def route1(self) -> list[Response | Effect]:
|
|
785
|
-
return []
|
|
786
|
-
|
|
787
|
-
@api.get("/route")
|
|
788
|
-
def route2(self) -> list[Response | Effect]:
|
|
789
|
-
return []
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
def test_invalid_prefix_error() -> None:
|
|
793
|
-
"""Test the enforcement of the error that occurs when an API has an invalid prefix."""
|
|
794
|
-
with pytest.raises(PluginError):
|
|
795
|
-
|
|
796
|
-
class API(APINoAuth):
|
|
797
|
-
PREFIX = "prefix"
|
|
798
|
-
|
|
799
|
-
@api.get("/route")
|
|
800
|
-
def route(self) -> list[Response | Effect]:
|
|
801
|
-
return []
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
def test_invalid_path_error() -> None:
|
|
805
|
-
"""Test the enforcement of the error that occurs when a route has an invalid path."""
|
|
806
|
-
with pytest.raises(PluginError):
|
|
807
|
-
|
|
808
|
-
class Route(RouteNoAuth):
|
|
809
|
-
PATH = "route"
|
|
810
|
-
|
|
811
|
-
def get(self) -> list[Response | Effect]:
|
|
812
|
-
return []
|
|
813
|
-
|
|
814
|
-
with pytest.raises(PluginError):
|
|
815
|
-
|
|
816
|
-
class API(APINoAuth):
|
|
817
|
-
@api.get("route")
|
|
818
|
-
def route(self) -> list[Response | Effect]:
|
|
819
|
-
return []
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
def test_invalid_path_pattern_error() -> None:
|
|
823
|
-
"""Test the enforcement of the error that occurs when a route has an invalid path pattern."""
|
|
824
|
-
with pytest.raises(PluginError):
|
|
825
|
-
|
|
826
|
-
class Route(RouteNoAuth):
|
|
827
|
-
PATH = "/path1/<value>/<path2>/<value>"
|
|
828
|
-
|
|
829
|
-
def get(self) -> list[Response | Effect]:
|
|
830
|
-
return []
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
def test_route_missing_path_error() -> None:
|
|
834
|
-
"""
|
|
835
|
-
Test the enforcement of the error that occurs when a SimpleAPIRoute is missing a PATH value.
|
|
836
|
-
"""
|
|
837
|
-
with pytest.raises(PluginError):
|
|
838
|
-
|
|
839
|
-
class Route(RouteNoAuth):
|
|
840
|
-
def get(self) -> list[Response | Effect]:
|
|
841
|
-
return []
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
def test_route_has_prefix_error() -> None:
|
|
845
|
-
"""Test the enforcement of the error that occurs when a SimpleAPIRoute has a PREFIX value."""
|
|
846
|
-
with pytest.raises(PluginError):
|
|
847
|
-
|
|
848
|
-
class Route(RouteNoAuth):
|
|
849
|
-
PREFIX = "/prefix"
|
|
850
|
-
PATH = "/route"
|
|
851
|
-
|
|
852
|
-
def get(self) -> list[Response | Effect]:
|
|
853
|
-
return []
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
def test_route_that_uses_api_decorator_error() -> None:
|
|
857
|
-
"""
|
|
858
|
-
Test the enforcement of the error that occurs when a SimpleAPIRoute uses the api decorator.
|
|
859
|
-
"""
|
|
860
|
-
with pytest.raises(PluginError):
|
|
861
|
-
|
|
862
|
-
class Route(RouteNoAuth):
|
|
863
|
-
PREFIX = "/prefix"
|
|
864
|
-
PATH = "/route"
|
|
865
|
-
|
|
866
|
-
def get(self) -> list[Response | Effect]:
|
|
867
|
-
return []
|
|
868
|
-
|
|
869
|
-
@api.get("/route")
|
|
870
|
-
def route(self) -> list[Response | Effect]:
|
|
871
|
-
return []
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
def basic_headers(username: str, password: str) -> dict[str, str]:
|
|
875
|
-
"""Given a username and password, return headers that include a basic authentication header."""
|
|
876
|
-
return {"Authorization": f"Basic {b64encode(f'{username}:{password}'.encode()).decode()}"}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
def bearer_headers(token: str) -> dict[str, str]:
|
|
880
|
-
"""Given a token, return headers that include a bearer authentication header."""
|
|
881
|
-
return {"Authorization": f"Bearer {token}"}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
def api_key_headers(api_key: str) -> dict[str, str]:
|
|
885
|
-
"""Given an API key, return headers that include an API key authentication header."""
|
|
886
|
-
return {"Authorization": api_key}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
def custom_headers(api_key: str, app_key: str) -> dict[str, str]:
|
|
890
|
-
"""
|
|
891
|
-
Given an API key and an app key, return headers that include custom authentication headers.
|
|
892
|
-
"""
|
|
893
|
-
return {"API-Key": api_key, "App-Key": app_key}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
def session_headers(id: str, type: str) -> dict[str, str]:
|
|
897
|
-
"""
|
|
898
|
-
Given an id and a type, return headers that include the expected session based auth headers.
|
|
899
|
-
"""
|
|
900
|
-
return {"canvas-logged-in-user-type": type, "canvas-logged-in-user-id": id}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
USERNAME = uuid4().hex
|
|
904
|
-
PASSWORD = uuid4().hex
|
|
905
|
-
TOKEN = uuid4().hex
|
|
906
|
-
API_KEY = uuid4().hex
|
|
907
|
-
APP_KEY = uuid4().hex
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
@pytest.fixture(
|
|
911
|
-
params=[
|
|
912
|
-
(
|
|
913
|
-
BasicCredentials,
|
|
914
|
-
lambda _, credentials: credentials.username == USERNAME
|
|
915
|
-
and credentials.password == PASSWORD,
|
|
916
|
-
basic_headers(USERNAME, PASSWORD),
|
|
917
|
-
),
|
|
918
|
-
(
|
|
919
|
-
BearerCredentials,
|
|
920
|
-
lambda _, credentials: credentials.token == TOKEN,
|
|
921
|
-
bearer_headers(TOKEN),
|
|
922
|
-
),
|
|
923
|
-
(
|
|
924
|
-
APIKeyCredentials,
|
|
925
|
-
lambda _, credentials: credentials.key == API_KEY,
|
|
926
|
-
api_key_headers(API_KEY),
|
|
927
|
-
),
|
|
928
|
-
(
|
|
929
|
-
SessionCredentials,
|
|
930
|
-
lambda _, credentials: credentials.logged_in_user["type"] == "Staff",
|
|
931
|
-
session_headers("abc123", "Staff"),
|
|
932
|
-
),
|
|
933
|
-
(
|
|
934
|
-
Credentials,
|
|
935
|
-
lambda request, _: request.headers.get("API-Key") == API_KEY
|
|
936
|
-
and request.headers.get("App-Key") == APP_KEY,
|
|
937
|
-
custom_headers(API_KEY, APP_KEY),
|
|
938
|
-
),
|
|
939
|
-
],
|
|
940
|
-
ids=["basic", "bearer", "API key", "custom", "session"],
|
|
941
|
-
)
|
|
942
|
-
def authenticated_route(request: SubRequest) -> SimpleNamespace:
|
|
943
|
-
"""
|
|
944
|
-
Parametrized test fixture that returns a Route class with authentication.
|
|
945
|
-
|
|
946
|
-
It will also return a set of headers that will pass authentication for the route.
|
|
947
|
-
"""
|
|
948
|
-
credentials_cls, authenticate_impl, headers = request.param
|
|
949
|
-
|
|
950
|
-
class Route(SimpleAPIRoute):
|
|
951
|
-
PATH = "/route"
|
|
952
|
-
|
|
953
|
-
def authenticate(self, credentials: credentials_cls) -> bool: # type: ignore[valid-type]
|
|
954
|
-
return authenticate_impl(self.request, credentials)
|
|
955
|
-
|
|
956
|
-
def get(self) -> list[Response | Effect]:
|
|
957
|
-
return [Effect(type=EffectType.CREATE_TASK, payload="create task")]
|
|
958
|
-
|
|
959
|
-
return SimpleNamespace(cls=Route, headers=headers)
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
def test_authentication(authenticated_route: SimpleNamespace) -> None:
|
|
963
|
-
"""Test that valid credentials result in a successful response."""
|
|
964
|
-
effects = handle_request(
|
|
965
|
-
authenticated_route.cls, method="GET", path="/route", headers=authenticated_route.headers
|
|
966
|
-
)
|
|
967
|
-
|
|
968
|
-
assert effects == [Effect(type=EffectType.CREATE_TASK, payload="create task")]
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
@pytest.mark.parametrize(
|
|
972
|
-
argnames="headers",
|
|
973
|
-
argvalues=[
|
|
974
|
-
basic_headers(username=uuid4().hex, password=uuid4().hex),
|
|
975
|
-
basic_headers(username="", password=uuid4().hex),
|
|
976
|
-
basic_headers(username=uuid4().hex, password=""),
|
|
977
|
-
bearer_headers(token=uuid4().hex),
|
|
978
|
-
bearer_headers(token=""),
|
|
979
|
-
api_key_headers(api_key=uuid4().hex),
|
|
980
|
-
api_key_headers(api_key=""),
|
|
981
|
-
custom_headers(api_key=uuid4().hex, app_key=uuid4().hex),
|
|
982
|
-
custom_headers(api_key="", app_key=uuid4().hex),
|
|
983
|
-
custom_headers(api_key=uuid4().hex, app_key=""),
|
|
984
|
-
{},
|
|
985
|
-
],
|
|
986
|
-
ids=[
|
|
987
|
-
"basic",
|
|
988
|
-
"basic missing username",
|
|
989
|
-
"basic missing password",
|
|
990
|
-
"bearer",
|
|
991
|
-
"bearer missing token",
|
|
992
|
-
"API key",
|
|
993
|
-
"API key missing value",
|
|
994
|
-
"custom",
|
|
995
|
-
"custom missing API key",
|
|
996
|
-
"custom missing app key",
|
|
997
|
-
"no authentication headers",
|
|
998
|
-
],
|
|
999
|
-
)
|
|
1000
|
-
def test_authentication_failure(
|
|
1001
|
-
authenticated_route: SimpleNamespace, headers: Mapping[str, str]
|
|
1002
|
-
) -> None:
|
|
1003
|
-
"""Test that invalid credentials result in a failure response."""
|
|
1004
|
-
effects = handle_request(authenticated_route.cls, method="GET", path="/route", headers=headers)
|
|
1005
|
-
|
|
1006
|
-
assert json.loads(effects[0].payload)["status_code"] == HTTPStatus.UNAUTHORIZED
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
@pytest.mark.parametrize(
|
|
1010
|
-
argnames="credentials_cls,headers",
|
|
1011
|
-
argvalues=[
|
|
1012
|
-
(BasicCredentials, basic_headers(USERNAME, PASSWORD)),
|
|
1013
|
-
(BearerCredentials, bearer_headers(TOKEN)),
|
|
1014
|
-
(APIKeyCredentials, api_key_headers(API_KEY)),
|
|
1015
|
-
(Credentials, custom_headers(API_KEY, APP_KEY)),
|
|
1016
|
-
(SessionCredentials, session_headers("abc123", "Patient")),
|
|
1017
|
-
],
|
|
1018
|
-
ids=["basic", "bearer", "API key", "custom", "session"],
|
|
1019
|
-
)
|
|
1020
|
-
def test_authentication_exception(
|
|
1021
|
-
credentials_cls: type[Credentials], headers: Mapping[str, str]
|
|
1022
|
-
) -> None:
|
|
1023
|
-
"""Test that an exception occurring during authentication results in a failure response."""
|
|
1024
|
-
|
|
1025
|
-
class Route(SimpleAPIRoute):
|
|
1026
|
-
PATH = "/route"
|
|
1027
|
-
|
|
1028
|
-
def authenticate(self, credentials: credentials_cls) -> bool: # type: ignore[valid-type]
|
|
1029
|
-
raise RuntimeError
|
|
1030
|
-
|
|
1031
|
-
def get(self) -> list[Response | Effect]:
|
|
1032
|
-
return [Effect(type=EffectType.CREATE_TASK, payload="create task")]
|
|
1033
|
-
|
|
1034
|
-
effects = handle_request(Route, method="GET", path="/route", headers=headers)
|
|
1035
|
-
|
|
1036
|
-
assert effects == [Response(status_code=HTTPStatus.INTERNAL_SERVER_ERROR).apply()]
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
@pytest.mark.parametrize(
|
|
1040
|
-
argnames="mixin_cls,secrets,headers,expected_effects",
|
|
1041
|
-
argvalues=[
|
|
1042
|
-
(
|
|
1043
|
-
BasicAuthMixin,
|
|
1044
|
-
{"simpleapi-basic-username": USERNAME, "simpleapi-basic-password": PASSWORD},
|
|
1045
|
-
basic_headers(USERNAME, PASSWORD),
|
|
1046
|
-
[Effect(type=EffectType.CREATE_TASK, payload="create task")],
|
|
1047
|
-
),
|
|
1048
|
-
(
|
|
1049
|
-
BasicAuthMixin,
|
|
1050
|
-
{"simpleapi-basic-username": USERNAME, "simpleapi-basic-password": PASSWORD},
|
|
1051
|
-
basic_headers(uuid4().hex, uuid4().hex),
|
|
1052
|
-
[
|
|
1053
|
-
JSONResponse(
|
|
1054
|
-
content={"error": "Provided credentials are invalid"},
|
|
1055
|
-
status_code=HTTPStatus.UNAUTHORIZED,
|
|
1056
|
-
).apply()
|
|
1057
|
-
],
|
|
1058
|
-
),
|
|
1059
|
-
(
|
|
1060
|
-
BasicAuthMixin,
|
|
1061
|
-
{},
|
|
1062
|
-
basic_headers(USERNAME, PASSWORD),
|
|
1063
|
-
[
|
|
1064
|
-
JSONResponse(
|
|
1065
|
-
content={"error": "Provided credentials are invalid"},
|
|
1066
|
-
status_code=HTTPStatus.UNAUTHORIZED,
|
|
1067
|
-
).apply()
|
|
1068
|
-
],
|
|
1069
|
-
),
|
|
1070
|
-
(
|
|
1071
|
-
APIKeyAuthMixin,
|
|
1072
|
-
{"simpleapi-api-key": API_KEY},
|
|
1073
|
-
api_key_headers(API_KEY),
|
|
1074
|
-
[Effect(type=EffectType.CREATE_TASK, payload="create task")],
|
|
1075
|
-
),
|
|
1076
|
-
(
|
|
1077
|
-
APIKeyAuthMixin,
|
|
1078
|
-
{"simpleapi-api-key": API_KEY},
|
|
1079
|
-
api_key_headers(uuid4().hex),
|
|
1080
|
-
[
|
|
1081
|
-
JSONResponse(
|
|
1082
|
-
content={"error": "Provided credentials are invalid"},
|
|
1083
|
-
status_code=HTTPStatus.UNAUTHORIZED,
|
|
1084
|
-
).apply()
|
|
1085
|
-
],
|
|
1086
|
-
),
|
|
1087
|
-
(
|
|
1088
|
-
APIKeyAuthMixin,
|
|
1089
|
-
{},
|
|
1090
|
-
api_key_headers(API_KEY),
|
|
1091
|
-
[
|
|
1092
|
-
JSONResponse(
|
|
1093
|
-
content={"error": "Provided credentials are invalid"},
|
|
1094
|
-
status_code=HTTPStatus.UNAUTHORIZED,
|
|
1095
|
-
).apply()
|
|
1096
|
-
],
|
|
1097
|
-
),
|
|
1098
|
-
(
|
|
1099
|
-
StaffSessionAuthMixin,
|
|
1100
|
-
{},
|
|
1101
|
-
session_headers("abc123", "Staff"),
|
|
1102
|
-
[Effect(type=EffectType.CREATE_TASK, payload="create task")],
|
|
1103
|
-
),
|
|
1104
|
-
(
|
|
1105
|
-
StaffSessionAuthMixin,
|
|
1106
|
-
{},
|
|
1107
|
-
session_headers("abc123", "Patient"),
|
|
1108
|
-
[
|
|
1109
|
-
JSONResponse(
|
|
1110
|
-
content={"error": "Provided credentials are invalid"},
|
|
1111
|
-
status_code=HTTPStatus.UNAUTHORIZED,
|
|
1112
|
-
).apply()
|
|
1113
|
-
],
|
|
1114
|
-
),
|
|
1115
|
-
(
|
|
1116
|
-
PatientSessionAuthMixin,
|
|
1117
|
-
{},
|
|
1118
|
-
session_headers("abc123", "Patient"),
|
|
1119
|
-
[Effect(type=EffectType.CREATE_TASK, payload="create task")],
|
|
1120
|
-
),
|
|
1121
|
-
(
|
|
1122
|
-
PatientSessionAuthMixin,
|
|
1123
|
-
{},
|
|
1124
|
-
session_headers("abc123", "Staff"),
|
|
1125
|
-
[
|
|
1126
|
-
JSONResponse(
|
|
1127
|
-
content={"error": "Provided credentials are invalid"},
|
|
1128
|
-
status_code=HTTPStatus.UNAUTHORIZED,
|
|
1129
|
-
).apply()
|
|
1130
|
-
],
|
|
1131
|
-
),
|
|
1132
|
-
],
|
|
1133
|
-
ids=[
|
|
1134
|
-
"basic valid",
|
|
1135
|
-
"basic invalid",
|
|
1136
|
-
"basic missing secret",
|
|
1137
|
-
"API key valid",
|
|
1138
|
-
"API key invalid",
|
|
1139
|
-
"API key missing secret",
|
|
1140
|
-
"Staff session valid",
|
|
1141
|
-
"Staff session invalid",
|
|
1142
|
-
"Patient session valid",
|
|
1143
|
-
"Patient session invalid",
|
|
1144
|
-
],
|
|
1145
|
-
)
|
|
1146
|
-
def test_authentication_mixins(
|
|
1147
|
-
mixin_cls: type[AuthSchemeMixin],
|
|
1148
|
-
secrets: dict[str, str],
|
|
1149
|
-
headers: Mapping[str, str],
|
|
1150
|
-
expected_effects: Sequence[Effect],
|
|
1151
|
-
) -> None:
|
|
1152
|
-
"""
|
|
1153
|
-
Test that the provided authentication mixins behave correctly in success and failure scenarios.
|
|
1154
|
-
"""
|
|
1155
|
-
|
|
1156
|
-
class Route(mixin_cls, SimpleAPIRoute): # type: ignore[misc,valid-type]
|
|
1157
|
-
PATH = "/route"
|
|
1158
|
-
|
|
1159
|
-
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
1160
|
-
super().__init__(*args, **kwargs)
|
|
1161
|
-
self.secrets = secrets
|
|
1162
|
-
|
|
1163
|
-
def get(self) -> list[Response | Effect]:
|
|
1164
|
-
return [Effect(type=EffectType.CREATE_TASK, payload="create task")]
|
|
1165
|
-
|
|
1166
|
-
effects = handle_request(Route, method="GET", path="/route", headers=headers)
|
|
1167
|
-
assert effects == expected_effects
|