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

1""" 

2camcops_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 

28import json 

29import logging 

30from pprint import pformat 

31from typing import Any, Dict 

32from unittest import mock, TestCase 

33 

34# noinspection PyProtectedMember 

35from colander import Invalid, null, Schema 

36from pendulum import Duration 

37import phonenumbers 

38 

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) 

68 

69log = logging.getLogger(__name__) 

70 

71 

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 ) 

92 

93 

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) 

101 

102 self.serialize_deserialize(schema, appstruct) 

103 

104 

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 } 

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 

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) 

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 

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) 

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 

371class 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 

504class 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 

563class 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 

663class 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 

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

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 

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 ) 

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 

735class 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 

869class 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 

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" 

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 

957class 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 

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) 

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 

998class 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 

1012class 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 

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)