Coverage for cc_modules/tests/cc_forms_tests.py: 21%

1"""

2 camcops_server/cc_modules/tests/cc_forms_tests.py

3

4===============================================================================

5

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

8

9 This file is part of CamCOPS.

10

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.

15

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.

20

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/>.

23

24===============================================================================

25

26"""

27

28 import json

29 import logging

30 from pprint import pformat

31 from typing import Any, Dict

32 from unittest import mock, TestCase

33

34# noinspection PyProtectedMember

35 from colander import Invalid, null, Schema

36 from pendulum import Duration

37 import phonenumbers

38

39 from camcops_server.cc_modules.cc_baseconstants import TEMPLATE_DIR

40 from 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)

55 from camcops_server.cc_modules.cc_ipuse import IpContexts

56 from camcops_server.cc_modules.cc_pyramid import ViewParam

57 from camcops_server.cc_modules.cc_testfactories import (

58 Fake,

59 GroupFactory,

60 TaskScheduleFactory,

61 UserFactory,

62 UserGroupMembershipFactory,

63)

64 from camcops_server.cc_modules.cc_unittest import (

65 BasicDatabaseTestCase,

66 DemoRequestTestCase,

67)

68

69 log = logging.getLogger(__name__)

70

71

72 class 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 )

92

93

94 class 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)

101

102 self.serialize_deserialize(schema, appstruct)

103

104

105 class 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 }

117

118 with self.assertRaises(Invalid) as cm:

119 schema.deserialize(cstruct)

120

121 self.assertIn(

122 "'bad_key' is not a valid placeholder",

123 cm.exception.children[0].messages()[0],

124 )

125

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 }

137

138 with self.assertRaises(Invalid) as cm:

139 schema.deserialize(cstruct)

140

141 self.assertIn(

142 "Invalid email template", cm.exception.children[0].messages()[0]

143 )

144

145

146 class 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)

157

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 }

167

168 cstruct = schema.serialize(appstruct)

169 with self.assertRaises(Invalid) as cm:

170 schema.deserialize(cstruct)

171

172 self.assertIn(

173 "you must tick 'Allow clinician tasks'", cm.exception.messages()[0]

174 )

175

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 }

185

186 try:

187 schema.serialize(appstruct)

188 except Invalid:

189 self.fail("Validation failed unexpectedly")

190

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 }

200

201 cstruct = schema.serialize(appstruct)

202 with self.assertRaises(Invalid) as cm:

203 schema.deserialize(cstruct)

204

205 self.assertIn(

206 "must be more than zero days", cm.exception.messages()[0]

207 )

208

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 }

218

219 cstruct = schema.serialize(appstruct)

220 with self.assertRaises(Invalid) as cm:

221 schema.deserialize(cstruct)

222

223 self.assertIn(

224 "must be more than zero days", cm.exception.messages()[0]

225 )

226

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 }

236

237 cstruct = schema.serialize(appstruct)

238 with self.assertRaises(Invalid) as cm:

239 schema.deserialize(cstruct)

240

241 self.assertIn("must be zero or more days", cm.exception.messages()[0])

242

243

244 class 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)

253

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 }

262

263 cstruct = schema.serialize(appstruct)

264 with self.assertRaises(Invalid) as cm:

265 schema.deserialize(cstruct)

266

267 self.assertIn("prohibits commercial", cm.exception.messages()[0])

268

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)

277

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 }

286

287 cstruct = schema.serialize(appstruct)

288 with self.assertRaises(Invalid) as cm:

289 schema.deserialize(cstruct)

290

291 self.assertIn("prohibits clinical", cm.exception.messages()[0])

292

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)

301

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 }

310

311 cstruct = schema.serialize(appstruct)

312

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)

320

321 self.assertIn("prohibits educational", cm.exception.messages()[0])

322

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)

331

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 }

340

341 cstruct = schema.serialize(appstruct)

342 with self.assertRaises(Invalid) as cm:

343 schema.deserialize(cstruct)

344

345 self.assertIn("prohibits research", cm.exception.messages()[0])

346

347 def test_invalid_for_missing_ip_use(self) -> None:

348 group = GroupFactory(ip_use=None)

349 schedule = TaskScheduleFactory(group=group)

350

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 }

359

360 cstruct = schema.serialize(appstruct)

361 with self.assertRaises(Invalid) as cm:

362 schema.deserialize(cstruct)

363

364 self.assertIn(

365 f"The group '{group.name}' has no intellectual property "

366 f"settings",

367 cm.exception.messages()[0],

368 )

369

370

371 class DurationWidgetTests(TestCase):

372 def setUp(self) -> None:

373 super().setUp()

374 self.request = mock.Mock(gettext=lambda t: t)

375

376 def test_serialize_renders_template_with_values(self) -> None:

377 widget = DurationWidget(self.request)

378

379 field = mock.Mock()

380 field.renderer = mock.Mock()

381

382 cstruct = {"months": 1, "weeks": 2, "days": 3}

383

384 widget.serialize(field, cstruct, readonly=False)

385

386 args, kwargs = field.renderer.call_args

387

388 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/duration.pt")

389 self.assertFalse(kwargs["readonly"])

390

391 self.assertEqual(kwargs["months"], 1)

392 self.assertEqual(kwargs["weeks"], 2)

393 self.assertEqual(kwargs["days"], 3)

394

395 self.assertEqual(kwargs["field"], field)

396

397 def test_serialize_renders_readonly_template_with_values(self) -> None:

398 widget = DurationWidget(self.request)

399

400 field = mock.Mock()

401 field.renderer = mock.Mock()

402

403 cstruct = {"months": 1, "weeks": 2, "days": 3}

404

405 widget.serialize(field, cstruct, readonly=True)

406

407 args, kwargs = field.renderer.call_args

408

409 self.assertEqual(

410 args[0], f"{TEMPLATE_DIR}/deform/readonly/duration.pt"

411 )

412 self.assertTrue(kwargs["readonly"])

413

414 def test_serialize_renders_readonly_template_if_widget_is_readonly(

415 self,

416 ) -> None:

417 widget = DurationWidget(self.request, readonly=True)

418

419 field = mock.Mock()

420 field.renderer = mock.Mock()

421

422 cstruct = {"months": 1, "weeks": 2, "days": 3}

423

424 widget.serialize(field, cstruct)

425

426 args, kwargs = field.renderer.call_args

427

428 self.assertEqual(

429 args[0], f"{TEMPLATE_DIR}/deform/readonly/duration.pt"

430 )

431

432 def test_serialize_with_null_defaults_to_blank_values(self) -> None:

433 widget = DurationWidget(self.request)

434

435 field = mock.Mock()

436 field.renderer = mock.Mock()

437

438 widget.serialize(field, null)

439

440 args, kwargs = field.renderer.call_args

441

442 self.assertEqual(kwargs["months"], "")

443 self.assertEqual(kwargs["weeks"], "")

444 self.assertEqual(kwargs["days"], "")

445

446 def test_serialize_none_defaults_to_blank_values(self) -> None:

447 widget = DurationWidget(self.request)

448

449 field = mock.Mock()

450 field.renderer = mock.Mock()

451

452 widget.serialize(field, None)

453

454 args, kwargs = field.renderer.call_args

455

456 self.assertEqual(kwargs["months"], "")

457 self.assertEqual(kwargs["weeks"], "")

458 self.assertEqual(kwargs["days"], "")

459

460 def test_deserialize_returns_valid_values(self) -> None:

461 widget = DurationWidget(self.request)

462

463 pstruct = {"days": 1, "weeks": 2, "months": 3}

464

465 # noinspection PyTypeChecker

466 cstruct = widget.deserialize(None, pstruct)

467

468 self.assertEqual(cstruct["days"], 1)

469 self.assertEqual(cstruct["weeks"], 2)

470 self.assertEqual(cstruct["months"], 3)

471

472 def test_deserialize_defaults_to_zero_days(self) -> None:

473 widget = DurationWidget(self.request)

474

475 # noinspection PyTypeChecker

476 cstruct = widget.deserialize(None, {})

477

478 self.assertEqual(cstruct["days"], 0)

479

480 def test_deserialize_fails_validation(self) -> None:

481 widget = DurationWidget(self.request)

482

483 pstruct = {"days": "abc", "weeks": "def", "months": "ghi"}

484

485 with self.assertRaises(Invalid) as cm:

486 # noinspection PyTypeChecker

487 widget.deserialize(None, pstruct)

488

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)

502

503

504 class DurationTypeTests(TestCase):

505 def test_deserialize_valid_duration(self) -> None:

506 cstruct = {"days": 45}

507

508 duration_type = DurationType()

509 duration = duration_type.deserialize(None, cstruct)

510 assert duration is not None # for type checker

511

512 self.assertEqual(duration.days, 45)

513

514 def test_deserialize_none_returns_null(self) -> None:

515 duration_type = DurationType()

516 duration = duration_type.deserialize(None, None)

517 self.assertIsNone(duration)

518

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

524

525 self.assertEqual(duration.days, 37)

526

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

532

533 self.assertEqual(duration.days, 8)

534

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

540

541 self.assertEqual(duration.days, 31)

542

543 def test_serialize_valid_duration(self) -> None:

544 duration = Duration(days=47)

545

546 duration_type = DurationType()

547 cstruct = duration_type.serialize(None, duration)

548

549 # For type checker

550 assert cstruct not in (null,)

551 cstruct: Dict[Any, Any]

552

553 self.assertEqual(cstruct["days"], 3)

554 self.assertEqual(cstruct["months"], 1)

555 self.assertEqual(cstruct["weeks"], 2)

556

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)

561

562

563 class JsonWidgetTests(TestCase):

564 def setUp(self) -> None:

565 super().setUp()

566 self.request = mock.Mock(gettext=lambda t: t)

567

568 def test_serialize_renders_template_with_values(self) -> None:

569 widget = JsonWidget(self.request)

570

571 field = mock.Mock()

572 field.renderer = mock.Mock()

573

574 cstruct = json.dumps({"a": "1", "b": "2", "c": "3"})

575

576 widget.serialize(field, cstruct, readonly=False)

577

578 args, kwargs = field.renderer.call_args

579

580 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/json.pt")

581 self.assertFalse(kwargs["readonly"])

582

583 self.assertEqual(kwargs["cstruct"], cstruct)

584 self.assertEqual(kwargs["field"], field)

585

586 def test_serialize_renders_readonly_template_with_values(self) -> None:

587 widget = JsonWidget(self.request)

588

589 field = mock.Mock()

590 field.renderer = mock.Mock()

591

592 cstruct = json.dumps({"a": "1", "b": "2", "c": "3"})

593

594 widget.serialize(field, cstruct, readonly=True)

595

596 args, kwargs = field.renderer.call_args

597

598 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/readonly/json.pt")

599

600 self.assertEqual(kwargs["cstruct"], cstruct)

601 self.assertEqual(kwargs["field"], field)

602 self.assertTrue(kwargs["readonly"])

603

604 def test_serialize_renders_readonly_template_if_widget_is_readonly(

605 self,

606 ) -> None:

607 widget = JsonWidget(self.request, readonly=True)

608

609 field = mock.Mock()

610 field.renderer = mock.Mock()

611

612 json_text = json.dumps({"a": "1", "b": "2", "c": "3"})

613 widget.serialize(field, json_text)

614

615 args, kwargs = field.renderer.call_args

616

617 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/readonly/json.pt")

618

619 def test_serialize_with_null_defaults_to_empty_string(self) -> None:

620 widget = JsonWidget(self.request)

621

622 field = mock.Mock()

623 field.renderer = mock.Mock()

624

625 widget.serialize(field, null)

626

627 args, kwargs = field.renderer.call_args

628

629 self.assertEqual(kwargs["cstruct"], "")

630

631 def test_deserialize_passes_json(self) -> None:

632 widget = JsonWidget(self.request)

633

634 pstruct = json.dumps({"a": "1", "b": "2", "c": "3"})

635

636 # noinspection PyTypeChecker

637 cstruct = widget.deserialize(None, pstruct)

638

639 self.assertEqual(cstruct, pstruct)

640

641 def test_deserialize_defaults_to_empty_json_string(self) -> None:

642 widget = JsonWidget(self.request)

643

644 # noinspection PyTypeChecker

645 cstruct = widget.deserialize(None, "{}")

646

647 self.assertEqual(cstruct, "{}")

648

649 def test_deserialize_invalid_json_fails_validation(self) -> None:

650 widget = JsonWidget(self.request)

651

652 pstruct = "{"

653

654 with self.assertRaises(Invalid) as cm:

655 # noinspection PyTypeChecker

656 widget.deserialize(None, pstruct)

657

658 self.assertIn("Please enter valid JSON", cm.exception.messages()[0])

659

660 self.assertEqual(cm.exception.value, "{")

661

662

663 class JsonTypeTests(TestCase):

664 def test_deserialize_valid_json(self) -> None:

665 original = {"one": 1, "two": 2, "three": 3}

666

667 json_type = JsonType()

668 json_value = json_type.deserialize(None, json.dumps(original))

669 self.assertEqual(json_value, original)

670

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)

675

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)

680

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)

685

686 def test_serialize_valid_appstruct(self) -> None:

687 original = {"one": 1, "two": 2, "three": 3}

688

689 json_type = JsonType()

690 json_string = json_type.serialize(None, original)

691 self.assertEqual(json_string, json.dumps(original))

692

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)

697

698

699 class 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({})

704

705 self.assertIn(

706 "Please enter a valid JSON object", cm.exception.messages()[0]

707 )

708

709 self.assertEqual(cm.exception.value, "[{}]")

710

711

712 class 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 )

720

721 my_schedule = TaskScheduleFactory(group=my_group)

722 not_my_schedule = TaskScheduleFactory(group=not_my_group)

723

724 self.req._debugging_user = user

725

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 )

733

734

735 class GroupIpUseWidgetTests(TestCase):

736 def setUp(self) -> None:

737 super().setUp()

738 self.request = mock.Mock(gettext=lambda t: t)

739

740 def test_serialize_renders_template_with_values(self) -> None:

741 widget = GroupIpUseWidget(self.request)

742

743 field = mock.Mock()

744 field.renderer = mock.Mock()

745

746 cstruct = {

747 IpContexts.CLINICAL: False,

748 IpContexts.COMMERCIAL: False,

749 IpContexts.EDUCATIONAL: True,

750 IpContexts.RESEARCH: True,

751 }

752

753 widget.serialize(field, cstruct, readonly=False)

754

755 args, kwargs = field.renderer.call_args

756

757 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/group_ip_use.pt")

758 self.assertFalse(kwargs["readonly"])

759

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)

765

766 def test_serialize_renders_readonly_template(self) -> None:

767 widget = GroupIpUseWidget(self.request)

768

769 field = mock.Mock()

770 field.renderer = mock.Mock()

771

772 cstruct = {

773 IpContexts.CLINICAL: False,

774 IpContexts.COMMERCIAL: False,

775 IpContexts.EDUCATIONAL: True,

776 IpContexts.RESEARCH: True,

777 }

778

779 widget.serialize(field, cstruct, readonly=True)

780

781 args, kwargs = field.renderer.call_args

782

783 self.assertEqual(

784 args[0], f"{TEMPLATE_DIR}/deform/readonly/group_ip_use.pt"

785 )

786 self.assertTrue(kwargs["readonly"])

787

788 def test_serialize_readonly_widget_renders_readonly_template(self) -> None:

789 widget = GroupIpUseWidget(self.request, readonly=True)

790

791 field = mock.Mock()

792 field.renderer = mock.Mock()

793

794 cstruct = {

795 IpContexts.CLINICAL: False,

796 IpContexts.COMMERCIAL: False,

797 IpContexts.EDUCATIONAL: True,

798 IpContexts.RESEARCH: True,

799 }

800

801 widget.serialize(field, cstruct)

802

803 args, kwargs = field.renderer.call_args

804

805 self.assertEqual(

806 args[0], f"{TEMPLATE_DIR}/deform/readonly/group_ip_use.pt"

807 )

808

809 def test_serialize_with_null_defaults_to_false_values(self) -> None:

810 widget = GroupIpUseWidget(self.request)

811

812 field = mock.Mock()

813 field.renderer = mock.Mock()

814

815 widget.serialize(field, null)

816

817 args, kwargs = field.renderer.call_args

818

819 self.assertFalse(kwargs[IpContexts.CLINICAL])

820 self.assertFalse(kwargs[IpContexts.COMMERCIAL])

821 self.assertFalse(kwargs[IpContexts.EDUCATIONAL])

822 self.assertFalse(kwargs[IpContexts.RESEARCH])

823

824 def test_serialize_with_none_defaults_to_false_values(self) -> None:

825 widget = GroupIpUseWidget(self.request)

826

827 field = mock.Mock()

828 field.renderer = mock.Mock()

829

830 widget.serialize(field, None)

831

832 args, kwargs = field.renderer.call_args

833

834 self.assertFalse(kwargs[IpContexts.CLINICAL])

835 self.assertFalse(kwargs[IpContexts.COMMERCIAL])

836 self.assertFalse(kwargs[IpContexts.EDUCATIONAL])

837 self.assertFalse(kwargs[IpContexts.RESEARCH])

838

839 def test_deserialize_with_null_defaults_to_false_values(self) -> None:

840 widget = GroupIpUseWidget(self.request)

841

842 field = None # Not used

843 # noinspection PyTypeChecker

844 cstruct = widget.deserialize(field, null)

845

846 self.assertFalse(cstruct[IpContexts.CLINICAL])

847 self.assertFalse(cstruct[IpContexts.COMMERCIAL])

848 self.assertFalse(cstruct[IpContexts.EDUCATIONAL])

849 self.assertFalse(cstruct[IpContexts.RESEARCH])

850

851 def test_deserialize_converts_to_bool_values(self) -> None:

852 widget = GroupIpUseWidget(self.request)

853

854 field = None # Not used

855

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"}

859

860 # noinspection PyTypeChecker

861 cstruct = widget.deserialize(field, pstruct)

862

863 self.assertFalse(cstruct[IpContexts.CLINICAL])

864 self.assertFalse(cstruct[IpContexts.COMMERCIAL])

865 self.assertTrue(cstruct[IpContexts.EDUCATIONAL])

866 self.assertTrue(cstruct[IpContexts.RESEARCH])

867

868

869 class IpUseTypeTests(TestCase):

870 def test_deserialize_none_returns_none(self) -> None:

871 ip_use_type = IpUseType()

872

873 node = None # not used

874 self.assertIsNone(ip_use_type.deserialize(node, None), None)

875

876 def test_deserialize_null_returns_none(self) -> None:

877 ip_use_type = IpUseType()

878

879 node = None # not used

880 self.assertIsNone(ip_use_type.deserialize(node, null), None)

881

882 def test_deserialize_returns_ip_use_object(self) -> None:

883 ip_use_type = IpUseType()

884

885 node = None # not used

886

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)

894

895 self.assertFalse(ip_use.clinical)

896 self.assertTrue(ip_use.commercial)

897 self.assertFalse(ip_use.educational)

898 self.assertTrue(ip_use.research)

899

900

901 class 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"

908

909 def test_serialize_renders_template_with_values(self) -> None:

910 widget = MfaSecretWidget(self.request)

911

912 field = mock.Mock()

913 field.renderer = mock.Mock()

914

915 cstruct = self.mfa_secret

916 widget.serialize(field, cstruct, readonly=False)

917

918 args, kwargs = field.renderer.call_args

919

920 self.assertEqual(args[0], f"{TEMPLATE_DIR}/deform/mfa_secret.pt")

921 self.assertFalse(kwargs["readonly"])

922

923 self.assertIn("<svg", kwargs["qr_code"])

924

925 def test_serialize_renders_readonly_template(self) -> None:

926 widget = MfaSecretWidget(self.request)

927

928 field = mock.Mock()

929 field.renderer = mock.Mock()

930

931 cstruct = self.mfa_secret

932 widget.serialize(field, cstruct, readonly=True)

933

934 args, kwargs = field.renderer.call_args

935

936 self.assertEqual(

937 args[0], f"{TEMPLATE_DIR}/deform/readonly/mfa_secret.pt"

938 )

939 self.assertTrue(kwargs["readonly"])

940

941 def test_serialize_readonly_widget_renders_readonly_template(self) -> None:

942 widget = MfaSecretWidget(self.request, readonly=True)

943

944 field = mock.Mock()

945 field.renderer = mock.Mock()

946

947 cstruct = self.mfa_secret

948 widget.serialize(field, cstruct)

949

950 args, kwargs = field.renderer.call_args

951

952 self.assertEqual(

953 args[0], f"{TEMPLATE_DIR}/deform/readonly/mfa_secret.pt"

954 )

955

956

957 class PhoneNumberTypeTestCase(TestCase):

958 def setUp(self) -> None:

959 super().setUp()

960

961 self.request = mock.Mock()

962 self.phone_type = PhoneNumberType(self.request, allow_empty=True)

963 self.node = mock.Mock()

964

965

966 class 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)

971

972 def test_raises_for_unparsable_number(self) -> None:

973 with self.assertRaises(Invalid) as cm:

974 self.phone_type.deserialize(self.node, "abc")

975

976 self.assertIn("Invalid phone number", cm.exception.messages()[0])

977

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")

981

982 self.assertIn("Invalid phone number", cm.exception.messages()[0])

983

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)

987

988 self.assertIsInstance(phone_number, phonenumbers.PhoneNumber)

989

990 self.assertEqual(

991 phonenumbers.format_number(

992 phone_number, phonenumbers.PhoneNumberFormat.E164

993 ),

994 phone_number_str,

995 )

996

997

998 class PhoneNumberTypeSerializeTests(PhoneNumberTypeTestCase):

999 def test_returns_null_for_appstruct_none(self) -> None:

1000 self.assertIs(self.phone_type.serialize(self.node, None), null)

1001

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)

1005

1006 self.assertEqual(

1007 self.phone_type.serialize(self.node, phone_number),

1008 phone_number_str,

1009 )

1010

1011

1012 class PhoneNumberTypeMandatoryTestCase(TestCase):

1013 def setUp(self) -> None:

1014 super().setUp()

1015

1016 self.request = mock.Mock()

1017 self.phone_type = PhoneNumberType(self.request, allow_empty=False)

1018 self.node = mock.Mock()

1019

1020

1021 class 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)