Coverage for cc_modules/tests/cc_forms_tests.py: 20%
528 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/cc_modules/tests/cc_forms_tests.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
13 CamCOPS is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 CamCOPS is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
26===============================================================================
28"""
30import json
31import logging
32from pprint import pformat
33from typing import Any, Dict
34from unittest import mock, TestCase
36# noinspection PyProtectedMember
37from colander import Invalid, null, Schema
38from pendulum import Duration
39import phonenumbers
41from camcops_server.cc_modules.cc_baseconstants import TEMPLATE_DIR
42from camcops_server.cc_modules.cc_forms import (
43 DurationType,
44 DurationWidget,
45 GroupIpUseWidget,
46 IpUseType,
47 MfaSecretWidget,
48 JsonType,
49 JsonWidget,
50 LoginSchema,
51 PhoneNumberType,
52 TaskScheduleItemSchema,
53 TaskScheduleNode,
54 TaskScheduleSchema,
55 TaskScheduleSelector,
56)
57from camcops_server.cc_modules.cc_ipuse import IpContexts
58from camcops_server.cc_modules.cc_pyramid import ViewParam
59from camcops_server.cc_modules.cc_taskschedule import TaskSchedule
60from camcops_server.cc_modules.cc_unittest import (
61 BasicDatabaseTestCase,
62 DemoDatabaseTestCase,
63 DemoRequestTestCase,
64)
66TEST_PHONE_NUMBER = "+{ctry}{tel}".format(
67 ctry=phonenumbers.PhoneMetadata.metadata_for_region("GB").country_code,
68 tel=phonenumbers.PhoneMetadata.metadata_for_region(
69 "GB"
70 ).personal_number.example_number,
71) # see webview_tests.py
73log = logging.getLogger(__name__)
76# =============================================================================
77# Unit tests
78# =============================================================================
81class SchemaTestCase(DemoRequestTestCase):
82 """
83 Unit tests.
84 """
86 def serialize_deserialize(
87 self, schema: Schema, appstruct: Dict[str, Any]
88 ) -> None:
89 cstruct = schema.serialize(appstruct)
90 final = schema.deserialize(cstruct)
91 mismatch = False
92 for k, v in appstruct.items():
93 if final[k] != v:
94 mismatch = True
95 break
96 self.assertFalse(
97 mismatch,
98 msg=(
99 "Elements of final don't match corresponding elements of "
100 "starting appstruct:\n"
101 f"final = {pformat(final)}\n"
102 f"start = {pformat(appstruct)}"
103 ),
104 )
107class LoginSchemaTests(SchemaTestCase):
108 def test_serialize_deserialize(self) -> None:
109 appstruct = {
110 ViewParam.USERNAME: "testuser",
111 ViewParam.PASSWORD: "testpw",
112 }
113 schema = LoginSchema().bind(request=self.req)
115 self.serialize_deserialize(schema, appstruct)
118class TaskScheduleSchemaTests(DemoDatabaseTestCase):
119 def test_invalid_for_bad_template_placeholder(self) -> None:
120 schema = TaskScheduleSchema().bind(request=self.req)
121 cstruct = {
122 ViewParam.NAME: "test",
123 ViewParam.GROUP_ID: str(self.group.id),
124 ViewParam.EMAIL_FROM: null,
125 ViewParam.EMAIL_CC: null,
126 ViewParam.EMAIL_BCC: null,
127 ViewParam.EMAIL_SUBJECT: "Subject",
128 ViewParam.EMAIL_TEMPLATE: "{bad_key}",
129 }
131 with self.assertRaises(Invalid) as cm:
132 schema.deserialize(cstruct)
134 self.assertIn(
135 "'bad_key' is not a valid placeholder",
136 cm.exception.children[0].messages()[0],
137 )
139 def test_invalid_for_mismatched_braces(self) -> None:
140 schema = TaskScheduleSchema().bind(request=self.req)
141 cstruct = {
142 ViewParam.NAME: "test",
143 ViewParam.GROUP_ID: str(self.group.id),
144 ViewParam.EMAIL_FROM: null,
145 ViewParam.EMAIL_CC: null,
146 ViewParam.EMAIL_BCC: null,
147 ViewParam.EMAIL_SUBJECT: "Subject",
148 ViewParam.EMAIL_TEMPLATE: "{server_url", # deliberately missing }
149 }
151 with self.assertRaises(Invalid) as cm:
152 schema.deserialize(cstruct)
154 self.assertIn(
155 "Invalid email template", cm.exception.children[0].messages()[0]
156 )
159class TaskScheduleItemSchemaTests(SchemaTestCase):
160 def test_serialize_deserialize(self) -> None:
161 appstruct = {
162 ViewParam.SCHEDULE_ID: 1,
163 ViewParam.TABLE_NAME: "bmi",
164 ViewParam.CLINICIAN_CONFIRMATION: False,
165 ViewParam.DUE_FROM: Duration(days=90),
166 ViewParam.DUE_WITHIN: Duration(days=100),
167 }
168 schema = TaskScheduleItemSchema().bind(request=self.req)
169 self.serialize_deserialize(schema, appstruct)
171 def test_invalid_for_clinician_task_with_no_confirmation(self) -> None:
172 schema = TaskScheduleItemSchema().bind(request=self.req)
173 appstruct = {
174 ViewParam.SCHEDULE_ID: 1,
175 ViewParam.TABLE_NAME: "elixhauserci",
176 ViewParam.CLINICIAN_CONFIRMATION: False,
177 ViewParam.DUE_FROM: Duration(days=90),
178 ViewParam.DUE_WITHIN: Duration(days=100),
179 }
181 cstruct = schema.serialize(appstruct)
182 with self.assertRaises(Invalid) as cm:
183 schema.deserialize(cstruct)
185 self.assertIn(
186 "you must tick 'Allow clinician tasks'", cm.exception.messages()[0]
187 )
189 def test_valid_for_clinician_task_with_confirmation(self) -> None:
190 schema = TaskScheduleItemSchema().bind(request=mock.Mock())
191 appstruct = {
192 ViewParam.SCHEDULE_ID: 1,
193 ViewParam.TABLE_NAME: "elixhauserci",
194 ViewParam.CLINICIAN_CONFIRMATION: True,
195 ViewParam.DUE_FROM: Duration(days=90),
196 ViewParam.DUE_WITHIN: Duration(days=100),
197 }
199 try:
200 schema.serialize(appstruct)
201 except Invalid:
202 self.fail("Validation failed unexpectedly")
204 def test_invalid_for_zero_due_within(self) -> None:
205 schema = TaskScheduleItemSchema().bind(request=self.req)
206 appstruct = {
207 ViewParam.SCHEDULE_ID: 1,
208 ViewParam.TABLE_NAME: "phq9",
209 ViewParam.CLINICIAN_CONFIRMATION: False,
210 ViewParam.DUE_FROM: Duration(days=90),
211 ViewParam.DUE_WITHIN: Duration(days=0),
212 }
214 cstruct = schema.serialize(appstruct)
215 with self.assertRaises(Invalid) as cm:
216 schema.deserialize(cstruct)
218 self.assertIn(
219 "must be more than zero days", cm.exception.messages()[0]
220 )
222 def test_invalid_for_negative_due_within(self) -> None:
223 schema = TaskScheduleItemSchema().bind(request=self.req)
224 appstruct = {
225 ViewParam.SCHEDULE_ID: 1,
226 ViewParam.TABLE_NAME: "phq9",
227 ViewParam.CLINICIAN_CONFIRMATION: False,
228 ViewParam.DUE_FROM: Duration(days=90),
229 ViewParam.DUE_WITHIN: Duration(days=-1),
230 }
232 cstruct = schema.serialize(appstruct)
233 with self.assertRaises(Invalid) as cm:
234 schema.deserialize(cstruct)
236 self.assertIn(
237 "must be more than zero days", cm.exception.messages()[0]
238 )
240 def test_invalid_for_negative_due_from(self) -> None:
241 schema = TaskScheduleItemSchema().bind(request=self.req)
242 appstruct = {
243 ViewParam.SCHEDULE_ID: 1,
244 ViewParam.TABLE_NAME: "phq9",
245 ViewParam.CLINICIAN_CONFIRMATION: False,
246 ViewParam.DUE_FROM: Duration(days=-1),
247 ViewParam.DUE_WITHIN: Duration(days=10),
248 }
250 cstruct = schema.serialize(appstruct)
251 with self.assertRaises(Invalid) as cm:
252 schema.deserialize(cstruct)
254 self.assertIn("must be zero or more days", cm.exception.messages()[0])
257class TaskScheduleItemSchemaIpTests(BasicDatabaseTestCase):
258 def setUp(self) -> None:
259 super().setUp()
261 self.schedule = TaskSchedule()
262 self.schedule.group_id = self.group.id
263 self.dbsession.add(self.schedule)
264 self.dbsession.commit()
266 def test_invalid_for_commercial_mismatch(self) -> None:
267 self.group.ip_use.commercial = True
268 self.dbsession.add(self.group)
269 self.dbsession.commit()
271 schema = TaskScheduleItemSchema().bind(request=self.req)
272 appstruct = {
273 ViewParam.SCHEDULE_ID: self.schedule.id,
274 ViewParam.TABLE_NAME: "mfi20",
275 ViewParam.CLINICIAN_CONFIRMATION: False,
276 ViewParam.DUE_FROM: Duration(days=0),
277 ViewParam.DUE_WITHIN: Duration(days=10),
278 }
280 cstruct = schema.serialize(appstruct)
281 with self.assertRaises(Invalid) as cm:
282 schema.deserialize(cstruct)
284 self.assertIn("prohibits commercial", cm.exception.messages()[0])
286 def test_invalid_for_clinical_mismatch(self) -> None:
287 self.group.ip_use.clinical = True
288 self.dbsession.add(self.group)
289 self.dbsession.commit()
291 schema = TaskScheduleItemSchema().bind(request=self.req)
292 appstruct = {
293 ViewParam.SCHEDULE_ID: self.schedule.id,
294 ViewParam.TABLE_NAME: "mfi20",
295 ViewParam.CLINICIAN_CONFIRMATION: False,
296 ViewParam.DUE_FROM: Duration(days=0),
297 ViewParam.DUE_WITHIN: Duration(days=10),
298 }
300 cstruct = schema.serialize(appstruct)
301 with self.assertRaises(Invalid) as cm:
302 schema.deserialize(cstruct)
304 self.assertIn("prohibits clinical", cm.exception.messages()[0])
306 def test_invalid_for_educational_mismatch(self) -> None:
307 self.group.ip_use.educational = True
308 self.dbsession.add(self.group)
309 self.dbsession.commit()
311 schema = TaskScheduleItemSchema().bind(request=self.req)
312 appstruct = {
313 ViewParam.SCHEDULE_ID: self.schedule.id,
314 ViewParam.TABLE_NAME: "mfi20",
315 ViewParam.CLINICIAN_CONFIRMATION: True,
316 ViewParam.DUE_FROM: Duration(days=0),
317 ViewParam.DUE_WITHIN: Duration(days=10),
318 }
320 cstruct = schema.serialize(appstruct)
322 # No real world example prohibits educational use
323 mock_task_class = mock.Mock(prohibits_educational=True)
324 with mock.patch.object(
325 schema, "_get_task_class", return_value=mock_task_class
326 ):
327 with self.assertRaises(Invalid) as cm:
328 schema.deserialize(cstruct)
330 self.assertIn("prohibits educational", cm.exception.messages()[0])
332 def test_invalid_for_research_mismatch(self) -> None:
333 self.group.ip_use.research = True
334 self.dbsession.add(self.group)
335 self.dbsession.commit()
337 schema = TaskScheduleItemSchema().bind(request=self.req)
338 appstruct = {
339 ViewParam.SCHEDULE_ID: self.schedule.id,
340 ViewParam.TABLE_NAME: "moca",
341 ViewParam.CLINICIAN_CONFIRMATION: True,
342 ViewParam.DUE_FROM: Duration(days=0),
343 ViewParam.DUE_WITHIN: Duration(days=10),
344 }
346 cstruct = schema.serialize(appstruct)
347 with self.assertRaises(Invalid) as cm:
348 schema.deserialize(cstruct)
350 self.assertIn("prohibits research", cm.exception.messages()[0])
352 def test_invalid_for_missing_ip_use(self) -> None:
353 self.group.ip_use = None
354 self.dbsession.add(self.group)
355 self.dbsession.commit()
357 schema = TaskScheduleItemSchema().bind(request=self.req)
358 appstruct = {
359 ViewParam.SCHEDULE_ID: self.schedule.id,
360 ViewParam.TABLE_NAME: "moca",
361 ViewParam.CLINICIAN_CONFIRMATION: True,
362 ViewParam.DUE_FROM: Duration(days=0),
363 ViewParam.DUE_WITHIN: Duration(days=10),
364 }
366 cstruct = schema.serialize(appstruct)
367 with self.assertRaises(Invalid) as cm:
368 schema.deserialize(cstruct)
370 self.assertIn(
371 f"The group '{self.group.name}' has no intellectual property "
372 f"settings",
373 cm.exception.messages()[0],
374 )
377class DurationWidgetTests(TestCase):
378 def setUp(self) -> None:
379 super().setUp()
380 self.request = mock.Mock(gettext=lambda t: t)
382 def test_serialize_renders_template_with_values(self) -> None:
383 widget = DurationWidget(self.request)
385 field = mock.Mock()
386 field.renderer = mock.Mock()
388 cstruct = {"months": 1, "weeks": 2, "days": 3}
390 widget.serialize(field, cstruct, readonly=False)
392 args, kwargs = field.renderer.call_args
394 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/duration.pt")
395 self.assertFalse(kwargs["readonly"])
397 self.assertEqual(kwargs["months"], 1)
398 self.assertEqual(kwargs["weeks"], 2)
399 self.assertEqual(kwargs["days"], 3)
401 self.assertEqual(kwargs["field"], field)
403 def test_serialize_renders_readonly_template_with_values(self) -> None:
404 widget = DurationWidget(self.request)
406 field = mock.Mock()
407 field.renderer = mock.Mock()
409 cstruct = {"months": 1, "weeks": 2, "days": 3}
411 widget.serialize(field, cstruct, readonly=True)
413 args, kwargs = field.renderer.call_args
415 self.assertEqual(
416 args[0], f"{TEMPLATE_DIR}/deform/readonly/duration.pt"
417 )
418 self.assertTrue(kwargs["readonly"])
420 def test_serialize_renders_readonly_template_if_widget_is_readonly(
421 self,
422 ) -> None:
423 widget = DurationWidget(self.request, readonly=True)
425 field = mock.Mock()
426 field.renderer = mock.Mock()
428 cstruct = {"months": 1, "weeks": 2, "days": 3}
430 widget.serialize(field, cstruct)
432 args, kwargs = field.renderer.call_args
434 self.assertEqual(
435 args[0], f"{TEMPLATE_DIR}/deform/readonly/duration.pt"
436 )
438 def test_serialize_with_null_defaults_to_blank_values(self) -> None:
439 widget = DurationWidget(self.request)
441 field = mock.Mock()
442 field.renderer = mock.Mock()
444 widget.serialize(field, null)
446 args, kwargs = field.renderer.call_args
448 self.assertEqual(kwargs["months"], "")
449 self.assertEqual(kwargs["weeks"], "")
450 self.assertEqual(kwargs["days"], "")
452 def test_serialize_none_defaults_to_blank_values(self) -> None:
453 widget = DurationWidget(self.request)
455 field = mock.Mock()
456 field.renderer = mock.Mock()
458 widget.serialize(field, None)
460 args, kwargs = field.renderer.call_args
462 self.assertEqual(kwargs["months"], "")
463 self.assertEqual(kwargs["weeks"], "")
464 self.assertEqual(kwargs["days"], "")
466 def test_deserialize_returns_valid_values(self) -> None:
467 widget = DurationWidget(self.request)
469 pstruct = {"days": 1, "weeks": 2, "months": 3}
471 # noinspection PyTypeChecker
472 cstruct = widget.deserialize(None, pstruct)
474 self.assertEqual(cstruct["days"], 1)
475 self.assertEqual(cstruct["weeks"], 2)
476 self.assertEqual(cstruct["months"], 3)
478 def test_deserialize_defaults_to_zero_days(self) -> None:
479 widget = DurationWidget(self.request)
481 # noinspection PyTypeChecker
482 cstruct = widget.deserialize(None, {})
484 self.assertEqual(cstruct["days"], 0)
486 def test_deserialize_fails_validation(self) -> None:
487 widget = DurationWidget(self.request)
489 pstruct = {"days": "abc", "weeks": "def", "months": "ghi"}
491 with self.assertRaises(Invalid) as cm:
492 # noinspection PyTypeChecker
493 widget.deserialize(None, pstruct)
495 self.assertIn(
496 "Please enter a valid number of days or leave blank",
497 cm.exception.messages(),
498 )
499 self.assertIn(
500 "Please enter a valid number of weeks or leave blank",
501 cm.exception.messages(),
502 )
503 self.assertIn(
504 "Please enter a valid number of months or leave blank",
505 cm.exception.messages(),
506 )
507 self.assertEqual(cm.exception.value, pstruct)
510class DurationTypeTests(TestCase):
511 def test_deserialize_valid_duration(self) -> None:
512 cstruct = {"days": 45}
514 duration_type = DurationType()
515 duration = duration_type.deserialize(None, cstruct)
516 assert duration is not None # for type checker
518 self.assertEqual(duration.days, 45)
520 def test_deserialize_none_returns_null(self) -> None:
521 duration_type = DurationType()
522 duration = duration_type.deserialize(None, None)
523 self.assertIsNone(duration)
525 def test_deserialize_ignores_invalid_days(self) -> None:
526 duration_type = DurationType()
527 cstruct = {"days": "abc", "months": 1, "weeks": 1}
528 duration = duration_type.deserialize(None, cstruct)
529 assert duration is not None # for type checker
531 self.assertEqual(duration.days, 37)
533 def test_deserialize_ignores_invalid_months(self) -> None:
534 duration_type = DurationType()
535 cstruct = {"days": 1, "months": "abc", "weeks": 1}
536 duration = duration_type.deserialize(None, cstruct)
537 assert duration is not None # for type checker
539 self.assertEqual(duration.days, 8)
541 def test_deserialize_ignores_invalid_weeks(self) -> None:
542 duration_type = DurationType()
543 cstruct = {"days": 1, "months": 1, "weeks": "abc"}
544 duration = duration_type.deserialize(None, cstruct)
545 assert duration is not None # for type checker
547 self.assertEqual(duration.days, 31)
549 def test_serialize_valid_duration(self) -> None:
550 duration = Duration(days=47)
552 duration_type = DurationType()
553 cstruct = duration_type.serialize(None, duration)
555 # For type checker
556 assert cstruct not in (null,)
557 cstruct: Dict[Any, Any]
559 self.assertEqual(cstruct["days"], 3)
560 self.assertEqual(cstruct["months"], 1)
561 self.assertEqual(cstruct["weeks"], 2)
563 def test_serialize_null_returns_null(self) -> None:
564 duration_type = DurationType()
565 cstruct = duration_type.serialize(None, null)
566 self.assertIs(cstruct, null)
569class JsonWidgetTests(TestCase):
570 def setUp(self) -> None:
571 super().setUp()
572 self.request = mock.Mock(gettext=lambda t: t)
574 def test_serialize_renders_template_with_values(self) -> None:
575 widget = JsonWidget(self.request)
577 field = mock.Mock()
578 field.renderer = mock.Mock()
580 cstruct = json.dumps({"a": "1", "b": "2", "c": "3"})
582 widget.serialize(field, cstruct, readonly=False)
584 args, kwargs = field.renderer.call_args
586 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/json.pt")
587 self.assertFalse(kwargs["readonly"])
589 self.assertEqual(kwargs["cstruct"], cstruct)
590 self.assertEqual(kwargs["field"], field)
592 def test_serialize_renders_readonly_template_with_values(self) -> None:
593 widget = JsonWidget(self.request)
595 field = mock.Mock()
596 field.renderer = mock.Mock()
598 cstruct = json.dumps({"a": "1", "b": "2", "c": "3"})
600 widget.serialize(field, cstruct, readonly=True)
602 args, kwargs = field.renderer.call_args
604 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/readonly/json.pt")
606 self.assertEqual(kwargs["cstruct"], cstruct)
607 self.assertEqual(kwargs["field"], field)
608 self.assertTrue(kwargs["readonly"])
610 def test_serialize_renders_readonly_template_if_widget_is_readonly(
611 self,
612 ) -> None:
613 widget = JsonWidget(self.request, readonly=True)
615 field = mock.Mock()
616 field.renderer = mock.Mock()
618 json_text = json.dumps({"a": "1", "b": "2", "c": "3"})
619 widget.serialize(field, json_text)
621 args, kwargs = field.renderer.call_args
623 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/readonly/json.pt")
625 def test_serialize_with_null_defaults_to_empty_string(self) -> None:
626 widget = JsonWidget(self.request)
628 field = mock.Mock()
629 field.renderer = mock.Mock()
631 widget.serialize(field, null)
633 args, kwargs = field.renderer.call_args
635 self.assertEqual(kwargs["cstruct"], "")
637 def test_deserialize_passes_json(self) -> None:
638 widget = JsonWidget(self.request)
640 pstruct = json.dumps({"a": "1", "b": "2", "c": "3"})
642 # noinspection PyTypeChecker
643 cstruct = widget.deserialize(None, pstruct)
645 self.assertEqual(cstruct, pstruct)
647 def test_deserialize_defaults_to_empty_json_string(self) -> None:
648 widget = JsonWidget(self.request)
650 # noinspection PyTypeChecker
651 cstruct = widget.deserialize(None, "{}")
653 self.assertEqual(cstruct, "{}")
655 def test_deserialize_invalid_json_fails_validation(self) -> None:
656 widget = JsonWidget(self.request)
658 pstruct = "{"
660 with self.assertRaises(Invalid) as cm:
661 # noinspection PyTypeChecker
662 widget.deserialize(None, pstruct)
664 self.assertIn("Please enter valid JSON", cm.exception.messages()[0])
666 self.assertEqual(cm.exception.value, "{")
669class JsonTypeTests(TestCase):
670 def test_deserialize_valid_json(self) -> None:
671 original = {"one": 1, "two": 2, "three": 3}
673 json_type = JsonType()
674 json_value = json_type.deserialize(None, json.dumps(original))
675 self.assertEqual(json_value, original)
677 def test_deserialize_null_returns_none(self) -> None:
678 json_type = JsonType()
679 json_value = json_type.deserialize(None, null)
680 self.assertIsNone(json_value)
682 def test_deserialize_none_returns_null(self) -> None:
683 json_type = JsonType()
684 json_value = json_type.deserialize(None, None)
685 self.assertIsNone(json_value)
687 def test_deserialize_invalid_json_returns_none(self) -> None:
688 json_type = JsonType()
689 json_value = json_type.deserialize(None, "{")
690 self.assertIsNone(json_value)
692 def test_serialize_valid_appstruct(self) -> None:
693 original = {"one": 1, "two": 2, "three": 3}
695 json_type = JsonType()
696 json_string = json_type.serialize(None, original)
697 self.assertEqual(json_string, json.dumps(original))
699 def test_serialize_null_returns_null(self) -> None:
700 json_type = JsonType()
701 json_string = json_type.serialize(None, null)
702 self.assertIs(json_string, null)
705class TaskScheduleNodeTests(TestCase):
706 def test_deserialize_not_a_json_object_fails_validation(self) -> None:
707 node = TaskScheduleNode()
708 with self.assertRaises(Invalid) as cm:
709 node.deserialize({})
711 self.assertIn(
712 "Please enter a valid JSON object", cm.exception.messages()[0]
713 )
715 self.assertEqual(cm.exception.value, "[{}]")
718class TaskScheduleSelectorTests(BasicDatabaseTestCase):
719 def test_displays_only_users_schedules(self) -> None:
720 user = self.create_user(username="regular_user")
721 my_group = self.create_group("mygroup")
722 not_my_group = self.create_group("notmygroup")
723 self.dbsession.flush()
725 self.create_membership(user, my_group, may_manage_patients=True)
727 my_schedule = TaskSchedule()
728 my_schedule.group_id = my_group.id
729 my_schedule.name = "My group's schedule"
730 self.dbsession.add(my_schedule)
732 not_my_schedule = TaskSchedule()
733 not_my_schedule.group_id = not_my_group.id
734 not_my_schedule.name = "Not my group's schedule"
735 self.dbsession.add(not_my_schedule)
736 self.dbsession.commit()
738 self.req._debugging_user = user
740 selector = TaskScheduleSelector().bind(request=self.req)
741 self.assertIn(
742 (my_schedule.id, my_schedule.name), selector.widget.values
743 )
744 self.assertNotIn(
745 (not_my_schedule.id, not_my_schedule.name), selector.widget.values
746 )
749class GroupIpUseWidgetTests(TestCase):
750 def setUp(self) -> None:
751 super().setUp()
752 self.request = mock.Mock(gettext=lambda t: t)
754 def test_serialize_renders_template_with_values(self) -> None:
755 widget = GroupIpUseWidget(self.request)
757 field = mock.Mock()
758 field.renderer = mock.Mock()
760 cstruct = {
761 IpContexts.CLINICAL: False,
762 IpContexts.COMMERCIAL: False,
763 IpContexts.EDUCATIONAL: True,
764 IpContexts.RESEARCH: True,
765 }
767 widget.serialize(field, cstruct, readonly=False)
769 args, kwargs = field.renderer.call_args
771 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/group_ip_use.pt")
772 self.assertFalse(kwargs["readonly"])
774 self.assertFalse(kwargs[IpContexts.CLINICAL])
775 self.assertFalse(kwargs[IpContexts.COMMERCIAL])
776 self.assertTrue(kwargs[IpContexts.EDUCATIONAL])
777 self.assertTrue(kwargs[IpContexts.RESEARCH])
778 self.assertEqual(kwargs["field"], field)
780 def test_serialize_renders_readonly_template(self) -> None:
781 widget = GroupIpUseWidget(self.request)
783 field = mock.Mock()
784 field.renderer = mock.Mock()
786 cstruct = {
787 IpContexts.CLINICAL: False,
788 IpContexts.COMMERCIAL: False,
789 IpContexts.EDUCATIONAL: True,
790 IpContexts.RESEARCH: True,
791 }
793 widget.serialize(field, cstruct, readonly=True)
795 args, kwargs = field.renderer.call_args
797 self.assertEqual(
798 args[0], f"{TEMPLATE_DIR}/deform/readonly/group_ip_use.pt"
799 )
800 self.assertTrue(kwargs["readonly"])
802 def test_serialize_readonly_widget_renders_readonly_template(self) -> None:
803 widget = GroupIpUseWidget(self.request, readonly=True)
805 field = mock.Mock()
806 field.renderer = mock.Mock()
808 cstruct = {
809 IpContexts.CLINICAL: False,
810 IpContexts.COMMERCIAL: False,
811 IpContexts.EDUCATIONAL: True,
812 IpContexts.RESEARCH: True,
813 }
815 widget.serialize(field, cstruct)
817 args, kwargs = field.renderer.call_args
819 self.assertEqual(
820 args[0], f"{TEMPLATE_DIR}/deform/readonly/group_ip_use.pt"
821 )
823 def test_serialize_with_null_defaults_to_false_values(self) -> None:
824 widget = GroupIpUseWidget(self.request)
826 field = mock.Mock()
827 field.renderer = mock.Mock()
829 widget.serialize(field, null)
831 args, kwargs = field.renderer.call_args
833 self.assertFalse(kwargs[IpContexts.CLINICAL])
834 self.assertFalse(kwargs[IpContexts.COMMERCIAL])
835 self.assertFalse(kwargs[IpContexts.EDUCATIONAL])
836 self.assertFalse(kwargs[IpContexts.RESEARCH])
838 def test_serialize_with_none_defaults_to_false_values(self) -> None:
839 widget = GroupIpUseWidget(self.request)
841 field = mock.Mock()
842 field.renderer = mock.Mock()
844 widget.serialize(field, None)
846 args, kwargs = field.renderer.call_args
848 self.assertFalse(kwargs[IpContexts.CLINICAL])
849 self.assertFalse(kwargs[IpContexts.COMMERCIAL])
850 self.assertFalse(kwargs[IpContexts.EDUCATIONAL])
851 self.assertFalse(kwargs[IpContexts.RESEARCH])
853 def test_deserialize_with_null_defaults_to_false_values(self) -> None:
854 widget = GroupIpUseWidget(self.request)
856 field = None # Not used
857 # noinspection PyTypeChecker
858 cstruct = widget.deserialize(field, null)
860 self.assertFalse(cstruct[IpContexts.CLINICAL])
861 self.assertFalse(cstruct[IpContexts.COMMERCIAL])
862 self.assertFalse(cstruct[IpContexts.EDUCATIONAL])
863 self.assertFalse(cstruct[IpContexts.RESEARCH])
865 def test_deserialize_converts_to_bool_values(self) -> None:
866 widget = GroupIpUseWidget(self.request)
868 field = None # Not used
870 # It shouldn't matter what the values are set to so long as the keys
871 # are present. In practice the values will be set to "1"
872 pstruct = {IpContexts.EDUCATIONAL: "1", IpContexts.RESEARCH: "1"}
874 # noinspection PyTypeChecker
875 cstruct = widget.deserialize(field, pstruct)
877 self.assertFalse(cstruct[IpContexts.CLINICAL])
878 self.assertFalse(cstruct[IpContexts.COMMERCIAL])
879 self.assertTrue(cstruct[IpContexts.EDUCATIONAL])
880 self.assertTrue(cstruct[IpContexts.RESEARCH])
883class IpUseTypeTests(TestCase):
884 def test_deserialize_none_returns_none(self) -> None:
885 ip_use_type = IpUseType()
887 node = None # not used
888 self.assertIsNone(ip_use_type.deserialize(node, None), None)
890 def test_deserialize_null_returns_none(self) -> None:
891 ip_use_type = IpUseType()
893 node = None # not used
894 self.assertIsNone(ip_use_type.deserialize(node, null), None)
896 def test_deserialize_returns_ip_use_object(self) -> None:
897 ip_use_type = IpUseType()
899 node = None # not used
901 cstruct = {
902 IpContexts.CLINICAL: False,
903 IpContexts.COMMERCIAL: True,
904 IpContexts.EDUCATIONAL: False,
905 IpContexts.RESEARCH: True,
906 }
907 ip_use = ip_use_type.deserialize(node, cstruct)
909 self.assertFalse(ip_use.clinical)
910 self.assertTrue(ip_use.commercial)
911 self.assertFalse(ip_use.educational)
912 self.assertTrue(ip_use.research)
915class MfaSecretWidgetTests(TestCase):
916 def setUp(self) -> None:
917 super().setUp()
918 self.request = mock.Mock(
919 gettext=lambda t: t, user=mock.Mock(username="test")
920 )
921 self.mfa_secret = "HVIHV7TUFQPV7KAIJE2GSJTLTEAQIQSJ"
923 def test_serialize_renders_template_with_values(self) -> None:
924 widget = MfaSecretWidget(self.request)
926 field = mock.Mock()
927 field.renderer = mock.Mock()
929 cstruct = self.mfa_secret
930 widget.serialize(field, cstruct, readonly=False)
932 args, kwargs = field.renderer.call_args
934 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/mfa_secret.pt")
935 self.assertFalse(kwargs["readonly"])
937 self.assertIn("<svg", kwargs["qr_code"])
939 def test_serialize_renders_readonly_template(self) -> None:
940 widget = MfaSecretWidget(self.request)
942 field = mock.Mock()
943 field.renderer = mock.Mock()
945 cstruct = self.mfa_secret
946 widget.serialize(field, cstruct, readonly=True)
948 args, kwargs = field.renderer.call_args
950 self.assertEqual(
951 args[0], f"{TEMPLATE_DIR}/deform/readonly/mfa_secret.pt"
952 )
953 self.assertTrue(kwargs["readonly"])
955 def test_serialize_readonly_widget_renders_readonly_template(self) -> None:
956 widget = MfaSecretWidget(self.request, readonly=True)
958 field = mock.Mock()
959 field.renderer = mock.Mock()
961 cstruct = self.mfa_secret
962 widget.serialize(field, cstruct)
964 args, kwargs = field.renderer.call_args
966 self.assertEqual(
967 args[0], f"{TEMPLATE_DIR}/deform/readonly/mfa_secret.pt"
968 )
971class PhoneNumberTypeTestCase(TestCase):
972 def setUp(self) -> None:
973 super().setUp()
975 self.request = mock.Mock()
976 self.phone_type = PhoneNumberType(self.request, allow_empty=True)
977 self.node = mock.Mock()
980class PhoneNumberTypeDeserializeTests(PhoneNumberTypeTestCase):
981 def test_returns_null_for_null_cstruct(self) -> None:
982 # For allow_empty=True:
983 phone_number = self.phone_type.deserialize(self.node, null)
984 self.assertIs(phone_number, null)
986 def test_raises_for_unparsable_number(self) -> None:
987 with self.assertRaises(Invalid) as cm:
988 self.phone_type.deserialize(self.node, "abc")
990 self.assertIn("Invalid phone number", cm.exception.messages()[0])
992 def test_raises_for_invalid_parsable_number(self) -> None:
993 with self.assertRaises(Invalid) as cm:
994 self.phone_type.deserialize(self.node, "+4411349600")
996 self.assertIn("Invalid phone number", cm.exception.messages()[0])
998 def test_returns_valid_phone_number(self) -> None:
999 phone_number = self.phone_type.deserialize(
1000 self.node, TEST_PHONE_NUMBER
1001 )
1003 self.assertIsInstance(phone_number, phonenumbers.PhoneNumber)
1005 self.assertEqual(
1006 phonenumbers.format_number(
1007 phone_number, phonenumbers.PhoneNumberFormat.E164
1008 ),
1009 TEST_PHONE_NUMBER,
1010 )
1013class PhoneNumberTypeSerializeTests(PhoneNumberTypeTestCase):
1014 def test_returns_null_for_appstruct_none(self) -> None:
1015 self.assertIs(self.phone_type.serialize(self.node, None), null)
1017 def test_returns_number_formatted_e164(self) -> None:
1018 phone_number = phonenumbers.parse(TEST_PHONE_NUMBER)
1020 self.assertEqual(
1021 self.phone_type.serialize(self.node, phone_number),
1022 TEST_PHONE_NUMBER,
1023 )
1026class PhoneNumberTypeMandatoryTestCase(TestCase):
1027 def setUp(self) -> None:
1028 super().setUp()
1030 self.request = mock.Mock()
1031 self.phone_type = PhoneNumberType(self.request, allow_empty=False)
1032 self.node = mock.Mock()
1035class PhoneNumberTypeMandatoryDeserializeTests(
1036 PhoneNumberTypeMandatoryTestCase
1037):
1038 def test_raises_for_appstruct_none(self) -> None:
1039 # For allow_empty=False:
1040 with self.assertRaises(Invalid):
1041 self.phone_type.deserialize(self.node, null)