Coverage for cc_modules/tests/cc_forms_tests.py: 21%
509 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-30 13:48 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-30 13:48 +0000
1"""
2camcops_server/cc_modules/tests/cc_forms_tests.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
11 CamCOPS is free software: you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation, either version 3 of the License, or
14 (at your option) any later version.
16 CamCOPS is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
24===============================================================================
26"""
28import json
29import logging
30from pprint import pformat
31from typing import Any, Dict
32from unittest import mock, TestCase
34# noinspection PyProtectedMember
35from colander import Invalid, null, Schema
36from pendulum import Duration
37import phonenumbers
39from camcops_server.cc_modules.cc_baseconstants import TEMPLATE_DIR
40from camcops_server.cc_modules.cc_forms import (
41 DurationType,
42 DurationWidget,
43 GroupIpUseWidget,
44 IpUseType,
45 MfaSecretWidget,
46 JsonType,
47 JsonWidget,
48 LoginSchema,
49 PhoneNumberType,
50 TaskScheduleItemSchema,
51 TaskScheduleNode,
52 TaskScheduleSchema,
53 TaskScheduleSelector,
54)
55from camcops_server.cc_modules.cc_ipuse import IpContexts
56from camcops_server.cc_modules.cc_pyramid import ViewParam
57from camcops_server.cc_modules.cc_testfactories import (
58 Fake,
59 GroupFactory,
60 TaskScheduleFactory,
61 UserFactory,
62 UserGroupMembershipFactory,
63)
64from camcops_server.cc_modules.cc_unittest import (
65 BasicDatabaseTestCase,
66 DemoRequestTestCase,
67)
69log = logging.getLogger(__name__)
72class SchemaTestCase(DemoRequestTestCase):
73 def serialize_deserialize(
74 self, schema: Schema, appstruct: Dict[str, Any]
75 ) -> None:
76 cstruct = schema.serialize(appstruct)
77 final = schema.deserialize(cstruct)
78 mismatch = False
79 for k, v in appstruct.items():
80 if final[k] != v:
81 mismatch = True
82 break
83 self.assertFalse(
84 mismatch,
85 msg=(
86 "Elements of final don't match corresponding elements of "
87 "starting appstruct:\n"
88 f"final = {pformat(final)}\n"
89 f"start = {pformat(appstruct)}"
90 ),
91 )
94class LoginSchemaTests(SchemaTestCase):
95 def test_serialize_deserialize(self) -> None:
96 appstruct = {
97 ViewParam.USERNAME: "testuser",
98 ViewParam.PASSWORD: "testpw",
99 }
100 schema = LoginSchema().bind(request=self.req)
102 self.serialize_deserialize(schema, appstruct)
105class TaskScheduleSchemaTests(BasicDatabaseTestCase):
106 def test_invalid_for_bad_template_placeholder(self) -> None:
107 schema = TaskScheduleSchema().bind(request=self.req)
108 cstruct = {
109 ViewParam.NAME: "test",
110 ViewParam.GROUP_ID: str(self.group.id),
111 ViewParam.EMAIL_FROM: null,
112 ViewParam.EMAIL_CC: null,
113 ViewParam.EMAIL_BCC: null,
114 ViewParam.EMAIL_SUBJECT: "Subject",
115 ViewParam.EMAIL_TEMPLATE: "{bad_key}",
116 }
118 with self.assertRaises(Invalid) as cm:
119 schema.deserialize(cstruct)
121 self.assertIn(
122 "'bad_key' is not a valid placeholder",
123 cm.exception.children[0].messages()[0],
124 )
126 def test_invalid_for_mismatched_braces(self) -> None:
127 schema = TaskScheduleSchema().bind(request=self.req)
128 cstruct = {
129 ViewParam.NAME: "test",
130 ViewParam.GROUP_ID: str(self.group.id),
131 ViewParam.EMAIL_FROM: null,
132 ViewParam.EMAIL_CC: null,
133 ViewParam.EMAIL_BCC: null,
134 ViewParam.EMAIL_SUBJECT: "Subject",
135 ViewParam.EMAIL_TEMPLATE: "{server_url", # deliberately missing }
136 }
138 with self.assertRaises(Invalid) as cm:
139 schema.deserialize(cstruct)
141 self.assertIn(
142 "Invalid email template", cm.exception.children[0].messages()[0]
143 )
146class TaskScheduleItemSchemaTests(SchemaTestCase):
147 def test_serialize_deserialize(self) -> None:
148 appstruct = {
149 ViewParam.SCHEDULE_ID: 1,
150 ViewParam.TABLE_NAME: "bmi",
151 ViewParam.CLINICIAN_CONFIRMATION: False,
152 ViewParam.DUE_FROM: Duration(days=90),
153 ViewParam.DUE_WITHIN: Duration(days=100),
154 }
155 schema = TaskScheduleItemSchema().bind(request=self.req)
156 self.serialize_deserialize(schema, appstruct)
158 def test_invalid_for_clinician_task_with_no_confirmation(self) -> None:
159 schema = TaskScheduleItemSchema().bind(request=self.req)
160 appstruct = {
161 ViewParam.SCHEDULE_ID: 1,
162 ViewParam.TABLE_NAME: "elixhauserci",
163 ViewParam.CLINICIAN_CONFIRMATION: False,
164 ViewParam.DUE_FROM: Duration(days=90),
165 ViewParam.DUE_WITHIN: Duration(days=100),
166 }
168 cstruct = schema.serialize(appstruct)
169 with self.assertRaises(Invalid) as cm:
170 schema.deserialize(cstruct)
172 self.assertIn(
173 "you must tick 'Allow clinician tasks'", cm.exception.messages()[0]
174 )
176 def test_valid_for_clinician_task_with_confirmation(self) -> None:
177 schema = TaskScheduleItemSchema().bind(request=mock.Mock())
178 appstruct = {
179 ViewParam.SCHEDULE_ID: 1,
180 ViewParam.TABLE_NAME: "elixhauserci",
181 ViewParam.CLINICIAN_CONFIRMATION: True,
182 ViewParam.DUE_FROM: Duration(days=90),
183 ViewParam.DUE_WITHIN: Duration(days=100),
184 }
186 try:
187 schema.serialize(appstruct)
188 except Invalid:
189 self.fail("Validation failed unexpectedly")
191 def test_invalid_for_zero_due_within(self) -> None:
192 schema = TaskScheduleItemSchema().bind(request=self.req)
193 appstruct = {
194 ViewParam.SCHEDULE_ID: 1,
195 ViewParam.TABLE_NAME: "phq9",
196 ViewParam.CLINICIAN_CONFIRMATION: False,
197 ViewParam.DUE_FROM: Duration(days=90),
198 ViewParam.DUE_WITHIN: Duration(days=0),
199 }
201 cstruct = schema.serialize(appstruct)
202 with self.assertRaises(Invalid) as cm:
203 schema.deserialize(cstruct)
205 self.assertIn(
206 "must be more than zero days", cm.exception.messages()[0]
207 )
209 def test_invalid_for_negative_due_within(self) -> None:
210 schema = TaskScheduleItemSchema().bind(request=self.req)
211 appstruct = {
212 ViewParam.SCHEDULE_ID: 1,
213 ViewParam.TABLE_NAME: "phq9",
214 ViewParam.CLINICIAN_CONFIRMATION: False,
215 ViewParam.DUE_FROM: Duration(days=90),
216 ViewParam.DUE_WITHIN: Duration(days=-1),
217 }
219 cstruct = schema.serialize(appstruct)
220 with self.assertRaises(Invalid) as cm:
221 schema.deserialize(cstruct)
223 self.assertIn(
224 "must be more than zero days", cm.exception.messages()[0]
225 )
227 def test_invalid_for_negative_due_from(self) -> None:
228 schema = TaskScheduleItemSchema().bind(request=self.req)
229 appstruct = {
230 ViewParam.SCHEDULE_ID: 1,
231 ViewParam.TABLE_NAME: "phq9",
232 ViewParam.CLINICIAN_CONFIRMATION: False,
233 ViewParam.DUE_FROM: Duration(days=-1),
234 ViewParam.DUE_WITHIN: Duration(days=10),
235 }
237 cstruct = schema.serialize(appstruct)
238 with self.assertRaises(Invalid) as cm:
239 schema.deserialize(cstruct)
241 self.assertIn("must be zero or more days", cm.exception.messages()[0])
244class TaskScheduleItemSchemaIpTests(DemoRequestTestCase):
245 def test_invalid_for_commercial_mismatch(self) -> None:
246 group = GroupFactory(
247 ip_use__clinical=False,
248 ip_use__commercial=True,
249 ip_use__educational=False,
250 ip_use__research=False,
251 )
252 schedule = TaskScheduleFactory(group=group)
254 schema = TaskScheduleItemSchema().bind(request=self.req)
255 appstruct = {
256 ViewParam.SCHEDULE_ID: schedule.id,
257 ViewParam.TABLE_NAME: "mfi20",
258 ViewParam.CLINICIAN_CONFIRMATION: False,
259 ViewParam.DUE_FROM: Duration(days=0),
260 ViewParam.DUE_WITHIN: Duration(days=10),
261 }
263 cstruct = schema.serialize(appstruct)
264 with self.assertRaises(Invalid) as cm:
265 schema.deserialize(cstruct)
267 self.assertIn("prohibits commercial", cm.exception.messages()[0])
269 def test_invalid_for_clinical_mismatch(self) -> None:
270 group = GroupFactory(
271 ip_use__clinical=True,
272 ip_use__commercial=False,
273 ip_use__educational=False,
274 ip_use__research=False,
275 )
276 schedule = TaskScheduleFactory(group=group)
278 schema = TaskScheduleItemSchema().bind(request=self.req)
279 appstruct = {
280 ViewParam.SCHEDULE_ID: schedule.id,
281 ViewParam.TABLE_NAME: "mfi20",
282 ViewParam.CLINICIAN_CONFIRMATION: False,
283 ViewParam.DUE_FROM: Duration(days=0),
284 ViewParam.DUE_WITHIN: Duration(days=10),
285 }
287 cstruct = schema.serialize(appstruct)
288 with self.assertRaises(Invalid) as cm:
289 schema.deserialize(cstruct)
291 self.assertIn("prohibits clinical", cm.exception.messages()[0])
293 def test_invalid_for_educational_mismatch(self) -> None:
294 group = GroupFactory(
295 ip_use__clinical=False,
296 ip_use__commercial=False,
297 ip_use__educational=True,
298 ip_use__research=False,
299 )
300 schedule = TaskScheduleFactory(group=group)
302 schema = TaskScheduleItemSchema().bind(request=self.req)
303 appstruct = {
304 ViewParam.SCHEDULE_ID: schedule.id,
305 ViewParam.TABLE_NAME: "mfi20",
306 ViewParam.CLINICIAN_CONFIRMATION: True,
307 ViewParam.DUE_FROM: Duration(days=0),
308 ViewParam.DUE_WITHIN: Duration(days=10),
309 }
311 cstruct = schema.serialize(appstruct)
313 # No real world example prohibits educational use
314 mock_task_class = mock.Mock(prohibits_educational=True)
315 with mock.patch.object(
316 schema, "_get_task_class", return_value=mock_task_class
317 ):
318 with self.assertRaises(Invalid) as cm:
319 schema.deserialize(cstruct)
321 self.assertIn("prohibits educational", cm.exception.messages()[0])
323 def test_invalid_for_research_mismatch(self) -> None:
324 group = GroupFactory(
325 ip_use__clinical=False,
326 ip_use__commercial=False,
327 ip_use__educational=False,
328 ip_use__research=True,
329 )
330 schedule = TaskScheduleFactory(group=group)
332 schema = TaskScheduleItemSchema().bind(request=self.req)
333 appstruct = {
334 ViewParam.SCHEDULE_ID: schedule.id,
335 ViewParam.TABLE_NAME: "moca",
336 ViewParam.CLINICIAN_CONFIRMATION: True,
337 ViewParam.DUE_FROM: Duration(days=0),
338 ViewParam.DUE_WITHIN: Duration(days=10),
339 }
341 cstruct = schema.serialize(appstruct)
342 with self.assertRaises(Invalid) as cm:
343 schema.deserialize(cstruct)
345 self.assertIn("prohibits research", cm.exception.messages()[0])
347 def test_invalid_for_missing_ip_use(self) -> None:
348 group = GroupFactory(ip_use=None)
349 schedule = TaskScheduleFactory(group=group)
351 schema = TaskScheduleItemSchema().bind(request=self.req)
352 appstruct = {
353 ViewParam.SCHEDULE_ID: schedule.id,
354 ViewParam.TABLE_NAME: "moca",
355 ViewParam.CLINICIAN_CONFIRMATION: True,
356 ViewParam.DUE_FROM: Duration(days=0),
357 ViewParam.DUE_WITHIN: Duration(days=10),
358 }
360 cstruct = schema.serialize(appstruct)
361 with self.assertRaises(Invalid) as cm:
362 schema.deserialize(cstruct)
364 self.assertIn(
365 f"The group '{group.name}' has no intellectual property "
366 f"settings",
367 cm.exception.messages()[0],
368 )
371class DurationWidgetTests(TestCase):
372 def setUp(self) -> None:
373 super().setUp()
374 self.request = mock.Mock(gettext=lambda t: t)
376 def test_serialize_renders_template_with_values(self) -> None:
377 widget = DurationWidget(self.request)
379 field = mock.Mock()
380 field.renderer = mock.Mock()
382 cstruct = {"months": 1, "weeks": 2, "days": 3}
384 widget.serialize(field, cstruct, readonly=False)
386 args, kwargs = field.renderer.call_args
388 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/duration.pt")
389 self.assertFalse(kwargs["readonly"])
391 self.assertEqual(kwargs["months"], 1)
392 self.assertEqual(kwargs["weeks"], 2)
393 self.assertEqual(kwargs["days"], 3)
395 self.assertEqual(kwargs["field"], field)
397 def test_serialize_renders_readonly_template_with_values(self) -> None:
398 widget = DurationWidget(self.request)
400 field = mock.Mock()
401 field.renderer = mock.Mock()
403 cstruct = {"months": 1, "weeks": 2, "days": 3}
405 widget.serialize(field, cstruct, readonly=True)
407 args, kwargs = field.renderer.call_args
409 self.assertEqual(
410 args[0], f"{TEMPLATE_DIR}/deform/readonly/duration.pt"
411 )
412 self.assertTrue(kwargs["readonly"])
414 def test_serialize_renders_readonly_template_if_widget_is_readonly(
415 self,
416 ) -> None:
417 widget = DurationWidget(self.request, readonly=True)
419 field = mock.Mock()
420 field.renderer = mock.Mock()
422 cstruct = {"months": 1, "weeks": 2, "days": 3}
424 widget.serialize(field, cstruct)
426 args, kwargs = field.renderer.call_args
428 self.assertEqual(
429 args[0], f"{TEMPLATE_DIR}/deform/readonly/duration.pt"
430 )
432 def test_serialize_with_null_defaults_to_blank_values(self) -> None:
433 widget = DurationWidget(self.request)
435 field = mock.Mock()
436 field.renderer = mock.Mock()
438 widget.serialize(field, null)
440 args, kwargs = field.renderer.call_args
442 self.assertEqual(kwargs["months"], "")
443 self.assertEqual(kwargs["weeks"], "")
444 self.assertEqual(kwargs["days"], "")
446 def test_serialize_none_defaults_to_blank_values(self) -> None:
447 widget = DurationWidget(self.request)
449 field = mock.Mock()
450 field.renderer = mock.Mock()
452 widget.serialize(field, None)
454 args, kwargs = field.renderer.call_args
456 self.assertEqual(kwargs["months"], "")
457 self.assertEqual(kwargs["weeks"], "")
458 self.assertEqual(kwargs["days"], "")
460 def test_deserialize_returns_valid_values(self) -> None:
461 widget = DurationWidget(self.request)
463 pstruct = {"days": 1, "weeks": 2, "months": 3}
465 # noinspection PyTypeChecker
466 cstruct = widget.deserialize(None, pstruct)
468 self.assertEqual(cstruct["days"], 1)
469 self.assertEqual(cstruct["weeks"], 2)
470 self.assertEqual(cstruct["months"], 3)
472 def test_deserialize_defaults_to_zero_days(self) -> None:
473 widget = DurationWidget(self.request)
475 # noinspection PyTypeChecker
476 cstruct = widget.deserialize(None, {})
478 self.assertEqual(cstruct["days"], 0)
480 def test_deserialize_fails_validation(self) -> None:
481 widget = DurationWidget(self.request)
483 pstruct = {"days": "abc", "weeks": "def", "months": "ghi"}
485 with self.assertRaises(Invalid) as cm:
486 # noinspection PyTypeChecker
487 widget.deserialize(None, pstruct)
489 self.assertIn(
490 "Please enter a valid number of days or leave blank",
491 cm.exception.messages(),
492 )
493 self.assertIn(
494 "Please enter a valid number of weeks or leave blank",
495 cm.exception.messages(),
496 )
497 self.assertIn(
498 "Please enter a valid number of months or leave blank",
499 cm.exception.messages(),
500 )
501 self.assertEqual(cm.exception.value, pstruct)
504class DurationTypeTests(TestCase):
505 def test_deserialize_valid_duration(self) -> None:
506 cstruct = {"days": 45}
508 duration_type = DurationType()
509 duration = duration_type.deserialize(None, cstruct)
510 assert duration is not None # for type checker
512 self.assertEqual(duration.days, 45)
514 def test_deserialize_none_returns_null(self) -> None:
515 duration_type = DurationType()
516 duration = duration_type.deserialize(None, None)
517 self.assertIsNone(duration)
519 def test_deserialize_ignores_invalid_days(self) -> None:
520 duration_type = DurationType()
521 cstruct = {"days": "abc", "months": 1, "weeks": 1}
522 duration = duration_type.deserialize(None, cstruct)
523 assert duration is not None # for type checker
525 self.assertEqual(duration.days, 37)
527 def test_deserialize_ignores_invalid_months(self) -> None:
528 duration_type = DurationType()
529 cstruct = {"days": 1, "months": "abc", "weeks": 1}
530 duration = duration_type.deserialize(None, cstruct)
531 assert duration is not None # for type checker
533 self.assertEqual(duration.days, 8)
535 def test_deserialize_ignores_invalid_weeks(self) -> None:
536 duration_type = DurationType()
537 cstruct = {"days": 1, "months": 1, "weeks": "abc"}
538 duration = duration_type.deserialize(None, cstruct)
539 assert duration is not None # for type checker
541 self.assertEqual(duration.days, 31)
543 def test_serialize_valid_duration(self) -> None:
544 duration = Duration(days=47)
546 duration_type = DurationType()
547 cstruct = duration_type.serialize(None, duration)
549 # For type checker
550 assert cstruct not in (null,)
551 cstruct: Dict[Any, Any]
553 self.assertEqual(cstruct["days"], 3)
554 self.assertEqual(cstruct["months"], 1)
555 self.assertEqual(cstruct["weeks"], 2)
557 def test_serialize_null_returns_null(self) -> None:
558 duration_type = DurationType()
559 cstruct = duration_type.serialize(None, null)
560 self.assertIs(cstruct, null)
563class JsonWidgetTests(TestCase):
564 def setUp(self) -> None:
565 super().setUp()
566 self.request = mock.Mock(gettext=lambda t: t)
568 def test_serialize_renders_template_with_values(self) -> None:
569 widget = JsonWidget(self.request)
571 field = mock.Mock()
572 field.renderer = mock.Mock()
574 cstruct = json.dumps({"a": "1", "b": "2", "c": "3"})
576 widget.serialize(field, cstruct, readonly=False)
578 args, kwargs = field.renderer.call_args
580 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/json.pt")
581 self.assertFalse(kwargs["readonly"])
583 self.assertEqual(kwargs["cstruct"], cstruct)
584 self.assertEqual(kwargs["field"], field)
586 def test_serialize_renders_readonly_template_with_values(self) -> None:
587 widget = JsonWidget(self.request)
589 field = mock.Mock()
590 field.renderer = mock.Mock()
592 cstruct = json.dumps({"a": "1", "b": "2", "c": "3"})
594 widget.serialize(field, cstruct, readonly=True)
596 args, kwargs = field.renderer.call_args
598 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/readonly/json.pt")
600 self.assertEqual(kwargs["cstruct"], cstruct)
601 self.assertEqual(kwargs["field"], field)
602 self.assertTrue(kwargs["readonly"])
604 def test_serialize_renders_readonly_template_if_widget_is_readonly(
605 self,
606 ) -> None:
607 widget = JsonWidget(self.request, readonly=True)
609 field = mock.Mock()
610 field.renderer = mock.Mock()
612 json_text = json.dumps({"a": "1", "b": "2", "c": "3"})
613 widget.serialize(field, json_text)
615 args, kwargs = field.renderer.call_args
617 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/readonly/json.pt")
619 def test_serialize_with_null_defaults_to_empty_string(self) -> None:
620 widget = JsonWidget(self.request)
622 field = mock.Mock()
623 field.renderer = mock.Mock()
625 widget.serialize(field, null)
627 args, kwargs = field.renderer.call_args
629 self.assertEqual(kwargs["cstruct"], "")
631 def test_deserialize_passes_json(self) -> None:
632 widget = JsonWidget(self.request)
634 pstruct = json.dumps({"a": "1", "b": "2", "c": "3"})
636 # noinspection PyTypeChecker
637 cstruct = widget.deserialize(None, pstruct)
639 self.assertEqual(cstruct, pstruct)
641 def test_deserialize_defaults_to_empty_json_string(self) -> None:
642 widget = JsonWidget(self.request)
644 # noinspection PyTypeChecker
645 cstruct = widget.deserialize(None, "{}")
647 self.assertEqual(cstruct, "{}")
649 def test_deserialize_invalid_json_fails_validation(self) -> None:
650 widget = JsonWidget(self.request)
652 pstruct = "{"
654 with self.assertRaises(Invalid) as cm:
655 # noinspection PyTypeChecker
656 widget.deserialize(None, pstruct)
658 self.assertIn("Please enter valid JSON", cm.exception.messages()[0])
660 self.assertEqual(cm.exception.value, "{")
663class JsonTypeTests(TestCase):
664 def test_deserialize_valid_json(self) -> None:
665 original = {"one": 1, "two": 2, "three": 3}
667 json_type = JsonType()
668 json_value = json_type.deserialize(None, json.dumps(original))
669 self.assertEqual(json_value, original)
671 def test_deserialize_null_returns_none(self) -> None:
672 json_type = JsonType()
673 json_value = json_type.deserialize(None, null)
674 self.assertIsNone(json_value)
676 def test_deserialize_none_returns_null(self) -> None:
677 json_type = JsonType()
678 json_value = json_type.deserialize(None, None)
679 self.assertIsNone(json_value)
681 def test_deserialize_invalid_json_returns_none(self) -> None:
682 json_type = JsonType()
683 json_value = json_type.deserialize(None, "{")
684 self.assertIsNone(json_value)
686 def test_serialize_valid_appstruct(self) -> None:
687 original = {"one": 1, "two": 2, "three": 3}
689 json_type = JsonType()
690 json_string = json_type.serialize(None, original)
691 self.assertEqual(json_string, json.dumps(original))
693 def test_serialize_null_returns_null(self) -> None:
694 json_type = JsonType()
695 json_string = json_type.serialize(None, null)
696 self.assertIs(json_string, null)
699class TaskScheduleNodeTests(TestCase):
700 def test_deserialize_not_a_json_object_fails_validation(self) -> None:
701 node = TaskScheduleNode()
702 with self.assertRaises(Invalid) as cm:
703 node.deserialize({})
705 self.assertIn(
706 "Please enter a valid JSON object", cm.exception.messages()[0]
707 )
709 self.assertEqual(cm.exception.value, "[{}]")
712class TaskScheduleSelectorTests(DemoRequestTestCase):
713 def test_displays_only_users_schedules(self) -> None:
714 user = UserFactory()
715 my_group = GroupFactory()
716 not_my_group = GroupFactory()
717 UserGroupMembershipFactory(
718 user_id=user.id, group_id=my_group.id, may_manage_patients=True
719 )
721 my_schedule = TaskScheduleFactory(group=my_group)
722 not_my_schedule = TaskScheduleFactory(group=not_my_group)
724 self.req._debugging_user = user
726 selector = TaskScheduleSelector().bind(request=self.req)
727 self.assertIn(
728 (my_schedule.id, my_schedule.name), selector.widget.values
729 )
730 self.assertNotIn(
731 (not_my_schedule.id, not_my_schedule.name), selector.widget.values
732 )
735class GroupIpUseWidgetTests(TestCase):
736 def setUp(self) -> None:
737 super().setUp()
738 self.request = mock.Mock(gettext=lambda t: t)
740 def test_serialize_renders_template_with_values(self) -> None:
741 widget = GroupIpUseWidget(self.request)
743 field = mock.Mock()
744 field.renderer = mock.Mock()
746 cstruct = {
747 IpContexts.CLINICAL: False,
748 IpContexts.COMMERCIAL: False,
749 IpContexts.EDUCATIONAL: True,
750 IpContexts.RESEARCH: True,
751 }
753 widget.serialize(field, cstruct, readonly=False)
755 args, kwargs = field.renderer.call_args
757 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/group_ip_use.pt")
758 self.assertFalse(kwargs["readonly"])
760 self.assertFalse(kwargs[IpContexts.CLINICAL])
761 self.assertFalse(kwargs[IpContexts.COMMERCIAL])
762 self.assertTrue(kwargs[IpContexts.EDUCATIONAL])
763 self.assertTrue(kwargs[IpContexts.RESEARCH])
764 self.assertEqual(kwargs["field"], field)
766 def test_serialize_renders_readonly_template(self) -> None:
767 widget = GroupIpUseWidget(self.request)
769 field = mock.Mock()
770 field.renderer = mock.Mock()
772 cstruct = {
773 IpContexts.CLINICAL: False,
774 IpContexts.COMMERCIAL: False,
775 IpContexts.EDUCATIONAL: True,
776 IpContexts.RESEARCH: True,
777 }
779 widget.serialize(field, cstruct, readonly=True)
781 args, kwargs = field.renderer.call_args
783 self.assertEqual(
784 args[0], f"{TEMPLATE_DIR}/deform/readonly/group_ip_use.pt"
785 )
786 self.assertTrue(kwargs["readonly"])
788 def test_serialize_readonly_widget_renders_readonly_template(self) -> None:
789 widget = GroupIpUseWidget(self.request, readonly=True)
791 field = mock.Mock()
792 field.renderer = mock.Mock()
794 cstruct = {
795 IpContexts.CLINICAL: False,
796 IpContexts.COMMERCIAL: False,
797 IpContexts.EDUCATIONAL: True,
798 IpContexts.RESEARCH: True,
799 }
801 widget.serialize(field, cstruct)
803 args, kwargs = field.renderer.call_args
805 self.assertEqual(
806 args[0], f"{TEMPLATE_DIR}/deform/readonly/group_ip_use.pt"
807 )
809 def test_serialize_with_null_defaults_to_false_values(self) -> None:
810 widget = GroupIpUseWidget(self.request)
812 field = mock.Mock()
813 field.renderer = mock.Mock()
815 widget.serialize(field, null)
817 args, kwargs = field.renderer.call_args
819 self.assertFalse(kwargs[IpContexts.CLINICAL])
820 self.assertFalse(kwargs[IpContexts.COMMERCIAL])
821 self.assertFalse(kwargs[IpContexts.EDUCATIONAL])
822 self.assertFalse(kwargs[IpContexts.RESEARCH])
824 def test_serialize_with_none_defaults_to_false_values(self) -> None:
825 widget = GroupIpUseWidget(self.request)
827 field = mock.Mock()
828 field.renderer = mock.Mock()
830 widget.serialize(field, None)
832 args, kwargs = field.renderer.call_args
834 self.assertFalse(kwargs[IpContexts.CLINICAL])
835 self.assertFalse(kwargs[IpContexts.COMMERCIAL])
836 self.assertFalse(kwargs[IpContexts.EDUCATIONAL])
837 self.assertFalse(kwargs[IpContexts.RESEARCH])
839 def test_deserialize_with_null_defaults_to_false_values(self) -> None:
840 widget = GroupIpUseWidget(self.request)
842 field = None # Not used
843 # noinspection PyTypeChecker
844 cstruct = widget.deserialize(field, null)
846 self.assertFalse(cstruct[IpContexts.CLINICAL])
847 self.assertFalse(cstruct[IpContexts.COMMERCIAL])
848 self.assertFalse(cstruct[IpContexts.EDUCATIONAL])
849 self.assertFalse(cstruct[IpContexts.RESEARCH])
851 def test_deserialize_converts_to_bool_values(self) -> None:
852 widget = GroupIpUseWidget(self.request)
854 field = None # Not used
856 # It shouldn't matter what the values are set to so long as the keys
857 # are present. In practice the values will be set to "1"
858 pstruct = {IpContexts.EDUCATIONAL: "1", IpContexts.RESEARCH: "1"}
860 # noinspection PyTypeChecker
861 cstruct = widget.deserialize(field, pstruct)
863 self.assertFalse(cstruct[IpContexts.CLINICAL])
864 self.assertFalse(cstruct[IpContexts.COMMERCIAL])
865 self.assertTrue(cstruct[IpContexts.EDUCATIONAL])
866 self.assertTrue(cstruct[IpContexts.RESEARCH])
869class IpUseTypeTests(TestCase):
870 def test_deserialize_none_returns_none(self) -> None:
871 ip_use_type = IpUseType()
873 node = None # not used
874 self.assertIsNone(ip_use_type.deserialize(node, None), None)
876 def test_deserialize_null_returns_none(self) -> None:
877 ip_use_type = IpUseType()
879 node = None # not used
880 self.assertIsNone(ip_use_type.deserialize(node, null), None)
882 def test_deserialize_returns_ip_use_object(self) -> None:
883 ip_use_type = IpUseType()
885 node = None # not used
887 cstruct = {
888 IpContexts.CLINICAL: False,
889 IpContexts.COMMERCIAL: True,
890 IpContexts.EDUCATIONAL: False,
891 IpContexts.RESEARCH: True,
892 }
893 ip_use = ip_use_type.deserialize(node, cstruct)
895 self.assertFalse(ip_use.clinical)
896 self.assertTrue(ip_use.commercial)
897 self.assertFalse(ip_use.educational)
898 self.assertTrue(ip_use.research)
901class MfaSecretWidgetTests(TestCase):
902 def setUp(self) -> None:
903 super().setUp()
904 self.request = mock.Mock(
905 gettext=lambda t: t, user=mock.Mock(username="test")
906 )
907 self.mfa_secret = "HVIHV7TUFQPV7KAIJE2GSJTLTEAQIQSJ"
909 def test_serialize_renders_template_with_values(self) -> None:
910 widget = MfaSecretWidget(self.request)
912 field = mock.Mock()
913 field.renderer = mock.Mock()
915 cstruct = self.mfa_secret
916 widget.serialize(field, cstruct, readonly=False)
918 args, kwargs = field.renderer.call_args
920 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/mfa_secret.pt")
921 self.assertFalse(kwargs["readonly"])
923 self.assertIn("<svg", kwargs["qr_code"])
925 def test_serialize_renders_readonly_template(self) -> None:
926 widget = MfaSecretWidget(self.request)
928 field = mock.Mock()
929 field.renderer = mock.Mock()
931 cstruct = self.mfa_secret
932 widget.serialize(field, cstruct, readonly=True)
934 args, kwargs = field.renderer.call_args
936 self.assertEqual(
937 args[0], f"{TEMPLATE_DIR}/deform/readonly/mfa_secret.pt"
938 )
939 self.assertTrue(kwargs["readonly"])
941 def test_serialize_readonly_widget_renders_readonly_template(self) -> None:
942 widget = MfaSecretWidget(self.request, readonly=True)
944 field = mock.Mock()
945 field.renderer = mock.Mock()
947 cstruct = self.mfa_secret
948 widget.serialize(field, cstruct)
950 args, kwargs = field.renderer.call_args
952 self.assertEqual(
953 args[0], f"{TEMPLATE_DIR}/deform/readonly/mfa_secret.pt"
954 )
957class PhoneNumberTypeTestCase(TestCase):
958 def setUp(self) -> None:
959 super().setUp()
961 self.request = mock.Mock()
962 self.phone_type = PhoneNumberType(self.request, allow_empty=True)
963 self.node = mock.Mock()
966class PhoneNumberTypeDeserializeTests(PhoneNumberTypeTestCase):
967 def test_returns_null_for_null_cstruct(self) -> None:
968 # For allow_empty=True:
969 phone_number = self.phone_type.deserialize(self.node, null)
970 self.assertIs(phone_number, null)
972 def test_raises_for_unparsable_number(self) -> None:
973 with self.assertRaises(Invalid) as cm:
974 self.phone_type.deserialize(self.node, "abc")
976 self.assertIn("Invalid phone number", cm.exception.messages()[0])
978 def test_raises_for_invalid_parsable_number(self) -> None:
979 with self.assertRaises(Invalid) as cm:
980 self.phone_type.deserialize(self.node, "+4411349600")
982 self.assertIn("Invalid phone number", cm.exception.messages()[0])
984 def test_returns_valid_phone_number(self) -> None:
985 phone_number_str = Fake.en_gb.valid_phone_number()
986 phone_number = self.phone_type.deserialize(self.node, phone_number_str)
988 self.assertIsInstance(phone_number, phonenumbers.PhoneNumber)
990 self.assertEqual(
991 phonenumbers.format_number(
992 phone_number, phonenumbers.PhoneNumberFormat.E164
993 ),
994 phone_number_str,
995 )
998class PhoneNumberTypeSerializeTests(PhoneNumberTypeTestCase):
999 def test_returns_null_for_appstruct_none(self) -> None:
1000 self.assertIs(self.phone_type.serialize(self.node, None), null)
1002 def test_returns_number_formatted_e164(self) -> None:
1003 phone_number_str = Fake.en_gb.valid_phone_number()
1004 phone_number = phonenumbers.parse(phone_number_str)
1006 self.assertEqual(
1007 self.phone_type.serialize(self.node, phone_number),
1008 phone_number_str,
1009 )
1012class PhoneNumberTypeMandatoryTestCase(TestCase):
1013 def setUp(self) -> None:
1014 super().setUp()
1016 self.request = mock.Mock()
1017 self.phone_type = PhoneNumberType(self.request, allow_empty=False)
1018 self.node = mock.Mock()
1021class PhoneNumberTypeMandatoryDeserializeTests(
1022 PhoneNumberTypeMandatoryTestCase
1023):
1024 def test_raises_for_appstruct_none(self) -> None:
1025 # For allow_empty=False:
1026 with self.assertRaises(Invalid):
1027 self.phone_type.deserialize(self.node, null)