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

528 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/cc_modules/tests/cc_forms_tests.py 

5 

6=============================================================================== 

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

13 CamCOPS is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CamCOPS is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

28""" 

29 

30import json 

31import logging 

32from pprint import pformat 

33from typing import Any, Dict 

34from unittest import mock, TestCase 

35 

36# noinspection PyProtectedMember 

37from colander import Invalid, null, Schema 

38from pendulum import Duration 

39import phonenumbers 

40 

41from camcops_server.cc_modules.cc_baseconstants import TEMPLATE_DIR 

42from camcops_server.cc_modules.cc_forms import ( 

43 DurationType, 

44 DurationWidget, 

45 GroupIpUseWidget, 

46 IpUseType, 

47 MfaSecretWidget, 

48 JsonType, 

49 JsonWidget, 

50 LoginSchema, 

51 PhoneNumberType, 

52 TaskScheduleItemSchema, 

53 TaskScheduleNode, 

54 TaskScheduleSchema, 

55 TaskScheduleSelector, 

56) 

57from camcops_server.cc_modules.cc_ipuse import IpContexts 

58from camcops_server.cc_modules.cc_pyramid import ViewParam 

59from camcops_server.cc_modules.cc_taskschedule import TaskSchedule 

60from camcops_server.cc_modules.cc_unittest import ( 

61 BasicDatabaseTestCase, 

62 DemoDatabaseTestCase, 

63 DemoRequestTestCase, 

64) 

65 

66TEST_PHONE_NUMBER = "+{ctry}{tel}".format( 

67 ctry=phonenumbers.PhoneMetadata.metadata_for_region("GB").country_code, 

68 tel=phonenumbers.PhoneMetadata.metadata_for_region( 

69 "GB" 

70 ).personal_number.example_number, 

71) # see webview_tests.py 

72 

73log = logging.getLogger(__name__) 

74 

75 

76# ============================================================================= 

77# Unit tests 

78# ============================================================================= 

79 

80 

81class SchemaTestCase(DemoRequestTestCase): 

82 """ 

83 Unit tests. 

84 """ 

85 

86 def serialize_deserialize( 

87 self, schema: Schema, appstruct: Dict[str, Any] 

88 ) -> None: 

89 cstruct = schema.serialize(appstruct) 

90 final = schema.deserialize(cstruct) 

91 mismatch = False 

92 for k, v in appstruct.items(): 

93 if final[k] != v: 

94 mismatch = True 

95 break 

96 self.assertFalse( 

97 mismatch, 

98 msg=( 

99 "Elements of final don't match corresponding elements of " 

100 "starting appstruct:\n" 

101 f"final = {pformat(final)}\n" 

102 f"start = {pformat(appstruct)}" 

103 ), 

104 ) 

105 

106 

107class LoginSchemaTests(SchemaTestCase): 

108 def test_serialize_deserialize(self) -> None: 

109 appstruct = { 

110 ViewParam.USERNAME: "testuser", 

111 ViewParam.PASSWORD: "testpw", 

112 } 

113 schema = LoginSchema().bind(request=self.req) 

114 

115 self.serialize_deserialize(schema, appstruct) 

116 

117 

118class TaskScheduleSchemaTests(DemoDatabaseTestCase): 

119 def test_invalid_for_bad_template_placeholder(self) -> None: 

120 schema = TaskScheduleSchema().bind(request=self.req) 

121 cstruct = { 

122 ViewParam.NAME: "test", 

123 ViewParam.GROUP_ID: str(self.group.id), 

124 ViewParam.EMAIL_FROM: null, 

125 ViewParam.EMAIL_CC: null, 

126 ViewParam.EMAIL_BCC: null, 

127 ViewParam.EMAIL_SUBJECT: "Subject", 

128 ViewParam.EMAIL_TEMPLATE: "{bad_key}", 

129 } 

130 

131 with self.assertRaises(Invalid) as cm: 

132 schema.deserialize(cstruct) 

133 

134 self.assertIn( 

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

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

137 ) 

138 

139 def test_invalid_for_mismatched_braces(self) -> None: 

140 schema = TaskScheduleSchema().bind(request=self.req) 

141 cstruct = { 

142 ViewParam.NAME: "test", 

143 ViewParam.GROUP_ID: str(self.group.id), 

144 ViewParam.EMAIL_FROM: null, 

145 ViewParam.EMAIL_CC: null, 

146 ViewParam.EMAIL_BCC: null, 

147 ViewParam.EMAIL_SUBJECT: "Subject", 

148 ViewParam.EMAIL_TEMPLATE: "{server_url", # deliberately missing } 

149 } 

150 

151 with self.assertRaises(Invalid) as cm: 

152 schema.deserialize(cstruct) 

153 

154 self.assertIn( 

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

156 ) 

157 

158 

159class TaskScheduleItemSchemaTests(SchemaTestCase): 

160 def test_serialize_deserialize(self) -> None: 

161 appstruct = { 

162 ViewParam.SCHEDULE_ID: 1, 

163 ViewParam.TABLE_NAME: "bmi", 

164 ViewParam.CLINICIAN_CONFIRMATION: False, 

165 ViewParam.DUE_FROM: Duration(days=90), 

166 ViewParam.DUE_WITHIN: Duration(days=100), 

167 } 

168 schema = TaskScheduleItemSchema().bind(request=self.req) 

169 self.serialize_deserialize(schema, appstruct) 

170 

171 def test_invalid_for_clinician_task_with_no_confirmation(self) -> None: 

172 schema = TaskScheduleItemSchema().bind(request=self.req) 

173 appstruct = { 

174 ViewParam.SCHEDULE_ID: 1, 

175 ViewParam.TABLE_NAME: "elixhauserci", 

176 ViewParam.CLINICIAN_CONFIRMATION: False, 

177 ViewParam.DUE_FROM: Duration(days=90), 

178 ViewParam.DUE_WITHIN: Duration(days=100), 

179 } 

180 

181 cstruct = schema.serialize(appstruct) 

182 with self.assertRaises(Invalid) as cm: 

183 schema.deserialize(cstruct) 

184 

185 self.assertIn( 

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

187 ) 

188 

189 def test_valid_for_clinician_task_with_confirmation(self) -> None: 

190 schema = TaskScheduleItemSchema().bind(request=mock.Mock()) 

191 appstruct = { 

192 ViewParam.SCHEDULE_ID: 1, 

193 ViewParam.TABLE_NAME: "elixhauserci", 

194 ViewParam.CLINICIAN_CONFIRMATION: True, 

195 ViewParam.DUE_FROM: Duration(days=90), 

196 ViewParam.DUE_WITHIN: Duration(days=100), 

197 } 

198 

199 try: 

200 schema.serialize(appstruct) 

201 except Invalid: 

202 self.fail("Validation failed unexpectedly") 

203 

204 def test_invalid_for_zero_due_within(self) -> None: 

205 schema = TaskScheduleItemSchema().bind(request=self.req) 

206 appstruct = { 

207 ViewParam.SCHEDULE_ID: 1, 

208 ViewParam.TABLE_NAME: "phq9", 

209 ViewParam.CLINICIAN_CONFIRMATION: False, 

210 ViewParam.DUE_FROM: Duration(days=90), 

211 ViewParam.DUE_WITHIN: Duration(days=0), 

212 } 

213 

214 cstruct = schema.serialize(appstruct) 

215 with self.assertRaises(Invalid) as cm: 

216 schema.deserialize(cstruct) 

217 

218 self.assertIn( 

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

220 ) 

221 

222 def test_invalid_for_negative_due_within(self) -> None: 

223 schema = TaskScheduleItemSchema().bind(request=self.req) 

224 appstruct = { 

225 ViewParam.SCHEDULE_ID: 1, 

226 ViewParam.TABLE_NAME: "phq9", 

227 ViewParam.CLINICIAN_CONFIRMATION: False, 

228 ViewParam.DUE_FROM: Duration(days=90), 

229 ViewParam.DUE_WITHIN: Duration(days=-1), 

230 } 

231 

232 cstruct = schema.serialize(appstruct) 

233 with self.assertRaises(Invalid) as cm: 

234 schema.deserialize(cstruct) 

235 

236 self.assertIn( 

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

238 ) 

239 

240 def test_invalid_for_negative_due_from(self) -> None: 

241 schema = TaskScheduleItemSchema().bind(request=self.req) 

242 appstruct = { 

243 ViewParam.SCHEDULE_ID: 1, 

244 ViewParam.TABLE_NAME: "phq9", 

245 ViewParam.CLINICIAN_CONFIRMATION: False, 

246 ViewParam.DUE_FROM: Duration(days=-1), 

247 ViewParam.DUE_WITHIN: Duration(days=10), 

248 } 

249 

250 cstruct = schema.serialize(appstruct) 

251 with self.assertRaises(Invalid) as cm: 

252 schema.deserialize(cstruct) 

253 

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

255 

256 

257class TaskScheduleItemSchemaIpTests(BasicDatabaseTestCase): 

258 def setUp(self) -> None: 

259 super().setUp() 

260 

261 self.schedule = TaskSchedule() 

262 self.schedule.group_id = self.group.id 

263 self.dbsession.add(self.schedule) 

264 self.dbsession.commit() 

265 

266 def test_invalid_for_commercial_mismatch(self) -> None: 

267 self.group.ip_use.commercial = True 

268 self.dbsession.add(self.group) 

269 self.dbsession.commit() 

270 

271 schema = TaskScheduleItemSchema().bind(request=self.req) 

272 appstruct = { 

273 ViewParam.SCHEDULE_ID: self.schedule.id, 

274 ViewParam.TABLE_NAME: "mfi20", 

275 ViewParam.CLINICIAN_CONFIRMATION: False, 

276 ViewParam.DUE_FROM: Duration(days=0), 

277 ViewParam.DUE_WITHIN: Duration(days=10), 

278 } 

279 

280 cstruct = schema.serialize(appstruct) 

281 with self.assertRaises(Invalid) as cm: 

282 schema.deserialize(cstruct) 

283 

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

285 

286 def test_invalid_for_clinical_mismatch(self) -> None: 

287 self.group.ip_use.clinical = True 

288 self.dbsession.add(self.group) 

289 self.dbsession.commit() 

290 

291 schema = TaskScheduleItemSchema().bind(request=self.req) 

292 appstruct = { 

293 ViewParam.SCHEDULE_ID: self.schedule.id, 

294 ViewParam.TABLE_NAME: "mfi20", 

295 ViewParam.CLINICIAN_CONFIRMATION: False, 

296 ViewParam.DUE_FROM: Duration(days=0), 

297 ViewParam.DUE_WITHIN: Duration(days=10), 

298 } 

299 

300 cstruct = schema.serialize(appstruct) 

301 with self.assertRaises(Invalid) as cm: 

302 schema.deserialize(cstruct) 

303 

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

305 

306 def test_invalid_for_educational_mismatch(self) -> None: 

307 self.group.ip_use.educational = True 

308 self.dbsession.add(self.group) 

309 self.dbsession.commit() 

310 

311 schema = TaskScheduleItemSchema().bind(request=self.req) 

312 appstruct = { 

313 ViewParam.SCHEDULE_ID: self.schedule.id, 

314 ViewParam.TABLE_NAME: "mfi20", 

315 ViewParam.CLINICIAN_CONFIRMATION: True, 

316 ViewParam.DUE_FROM: Duration(days=0), 

317 ViewParam.DUE_WITHIN: Duration(days=10), 

318 } 

319 

320 cstruct = schema.serialize(appstruct) 

321 

322 # No real world example prohibits educational use 

323 mock_task_class = mock.Mock(prohibits_educational=True) 

324 with mock.patch.object( 

325 schema, "_get_task_class", return_value=mock_task_class 

326 ): 

327 with self.assertRaises(Invalid) as cm: 

328 schema.deserialize(cstruct) 

329 

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

331 

332 def test_invalid_for_research_mismatch(self) -> None: 

333 self.group.ip_use.research = True 

334 self.dbsession.add(self.group) 

335 self.dbsession.commit() 

336 

337 schema = TaskScheduleItemSchema().bind(request=self.req) 

338 appstruct = { 

339 ViewParam.SCHEDULE_ID: self.schedule.id, 

340 ViewParam.TABLE_NAME: "moca", 

341 ViewParam.CLINICIAN_CONFIRMATION: True, 

342 ViewParam.DUE_FROM: Duration(days=0), 

343 ViewParam.DUE_WITHIN: Duration(days=10), 

344 } 

345 

346 cstruct = schema.serialize(appstruct) 

347 with self.assertRaises(Invalid) as cm: 

348 schema.deserialize(cstruct) 

349 

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

351 

352 def test_invalid_for_missing_ip_use(self) -> None: 

353 self.group.ip_use = None 

354 self.dbsession.add(self.group) 

355 self.dbsession.commit() 

356 

357 schema = TaskScheduleItemSchema().bind(request=self.req) 

358 appstruct = { 

359 ViewParam.SCHEDULE_ID: self.schedule.id, 

360 ViewParam.TABLE_NAME: "moca", 

361 ViewParam.CLINICIAN_CONFIRMATION: True, 

362 ViewParam.DUE_FROM: Duration(days=0), 

363 ViewParam.DUE_WITHIN: Duration(days=10), 

364 } 

365 

366 cstruct = schema.serialize(appstruct) 

367 with self.assertRaises(Invalid) as cm: 

368 schema.deserialize(cstruct) 

369 

370 self.assertIn( 

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

372 f"settings", 

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

374 ) 

375 

376 

377class DurationWidgetTests(TestCase): 

378 def setUp(self) -> None: 

379 super().setUp() 

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

381 

382 def test_serialize_renders_template_with_values(self) -> None: 

383 widget = DurationWidget(self.request) 

384 

385 field = mock.Mock() 

386 field.renderer = mock.Mock() 

387 

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

389 

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

391 

392 args, kwargs = field.renderer.call_args 

393 

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

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

396 

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

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

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

400 

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

402 

403 def test_serialize_renders_readonly_template_with_values(self) -> None: 

404 widget = DurationWidget(self.request) 

405 

406 field = mock.Mock() 

407 field.renderer = mock.Mock() 

408 

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

410 

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

412 

413 args, kwargs = field.renderer.call_args 

414 

415 self.assertEqual( 

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

417 ) 

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

419 

420 def test_serialize_renders_readonly_template_if_widget_is_readonly( 

421 self, 

422 ) -> None: 

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

424 

425 field = mock.Mock() 

426 field.renderer = mock.Mock() 

427 

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

429 

430 widget.serialize(field, cstruct) 

431 

432 args, kwargs = field.renderer.call_args 

433 

434 self.assertEqual( 

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

436 ) 

437 

438 def test_serialize_with_null_defaults_to_blank_values(self) -> None: 

439 widget = DurationWidget(self.request) 

440 

441 field = mock.Mock() 

442 field.renderer = mock.Mock() 

443 

444 widget.serialize(field, null) 

445 

446 args, kwargs = field.renderer.call_args 

447 

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

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

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

451 

452 def test_serialize_none_defaults_to_blank_values(self) -> None: 

453 widget = DurationWidget(self.request) 

454 

455 field = mock.Mock() 

456 field.renderer = mock.Mock() 

457 

458 widget.serialize(field, None) 

459 

460 args, kwargs = field.renderer.call_args 

461 

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

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

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

465 

466 def test_deserialize_returns_valid_values(self) -> None: 

467 widget = DurationWidget(self.request) 

468 

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

470 

471 # noinspection PyTypeChecker 

472 cstruct = widget.deserialize(None, pstruct) 

473 

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

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

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

477 

478 def test_deserialize_defaults_to_zero_days(self) -> None: 

479 widget = DurationWidget(self.request) 

480 

481 # noinspection PyTypeChecker 

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

483 

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

485 

486 def test_deserialize_fails_validation(self) -> None: 

487 widget = DurationWidget(self.request) 

488 

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

490 

491 with self.assertRaises(Invalid) as cm: 

492 # noinspection PyTypeChecker 

493 widget.deserialize(None, pstruct) 

494 

495 self.assertIn( 

496 "Please enter a valid number of days or leave blank", 

497 cm.exception.messages(), 

498 ) 

499 self.assertIn( 

500 "Please enter a valid number of weeks or leave blank", 

501 cm.exception.messages(), 

502 ) 

503 self.assertIn( 

504 "Please enter a valid number of months or leave blank", 

505 cm.exception.messages(), 

506 ) 

507 self.assertEqual(cm.exception.value, pstruct) 

508 

509 

510class DurationTypeTests(TestCase): 

511 def test_deserialize_valid_duration(self) -> None: 

512 cstruct = {"days": 45} 

513 

514 duration_type = DurationType() 

515 duration = duration_type.deserialize(None, cstruct) 

516 assert duration is not None # for type checker 

517 

518 self.assertEqual(duration.days, 45) 

519 

520 def test_deserialize_none_returns_null(self) -> None: 

521 duration_type = DurationType() 

522 duration = duration_type.deserialize(None, None) 

523 self.assertIsNone(duration) 

524 

525 def test_deserialize_ignores_invalid_days(self) -> None: 

526 duration_type = DurationType() 

527 cstruct = {"days": "abc", "months": 1, "weeks": 1} 

528 duration = duration_type.deserialize(None, cstruct) 

529 assert duration is not None # for type checker 

530 

531 self.assertEqual(duration.days, 37) 

532 

533 def test_deserialize_ignores_invalid_months(self) -> None: 

534 duration_type = DurationType() 

535 cstruct = {"days": 1, "months": "abc", "weeks": 1} 

536 duration = duration_type.deserialize(None, cstruct) 

537 assert duration is not None # for type checker 

538 

539 self.assertEqual(duration.days, 8) 

540 

541 def test_deserialize_ignores_invalid_weeks(self) -> None: 

542 duration_type = DurationType() 

543 cstruct = {"days": 1, "months": 1, "weeks": "abc"} 

544 duration = duration_type.deserialize(None, cstruct) 

545 assert duration is not None # for type checker 

546 

547 self.assertEqual(duration.days, 31) 

548 

549 def test_serialize_valid_duration(self) -> None: 

550 duration = Duration(days=47) 

551 

552 duration_type = DurationType() 

553 cstruct = duration_type.serialize(None, duration) 

554 

555 # For type checker 

556 assert cstruct not in (null,) 

557 cstruct: Dict[Any, Any] 

558 

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

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

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

562 

563 def test_serialize_null_returns_null(self) -> None: 

564 duration_type = DurationType() 

565 cstruct = duration_type.serialize(None, null) 

566 self.assertIs(cstruct, null) 

567 

568 

569class JsonWidgetTests(TestCase): 

570 def setUp(self) -> None: 

571 super().setUp() 

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

573 

574 def test_serialize_renders_template_with_values(self) -> None: 

575 widget = JsonWidget(self.request) 

576 

577 field = mock.Mock() 

578 field.renderer = mock.Mock() 

579 

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

581 

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

583 

584 args, kwargs = field.renderer.call_args 

585 

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

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

588 

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

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

591 

592 def test_serialize_renders_readonly_template_with_values(self) -> None: 

593 widget = JsonWidget(self.request) 

594 

595 field = mock.Mock() 

596 field.renderer = mock.Mock() 

597 

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

599 

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

601 

602 args, kwargs = field.renderer.call_args 

603 

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

605 

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

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

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

609 

610 def test_serialize_renders_readonly_template_if_widget_is_readonly( 

611 self, 

612 ) -> None: 

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

614 

615 field = mock.Mock() 

616 field.renderer = mock.Mock() 

617 

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

619 widget.serialize(field, json_text) 

620 

621 args, kwargs = field.renderer.call_args 

622 

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

624 

625 def test_serialize_with_null_defaults_to_empty_string(self) -> None: 

626 widget = JsonWidget(self.request) 

627 

628 field = mock.Mock() 

629 field.renderer = mock.Mock() 

630 

631 widget.serialize(field, null) 

632 

633 args, kwargs = field.renderer.call_args 

634 

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

636 

637 def test_deserialize_passes_json(self) -> None: 

638 widget = JsonWidget(self.request) 

639 

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

641 

642 # noinspection PyTypeChecker 

643 cstruct = widget.deserialize(None, pstruct) 

644 

645 self.assertEqual(cstruct, pstruct) 

646 

647 def test_deserialize_defaults_to_empty_json_string(self) -> None: 

648 widget = JsonWidget(self.request) 

649 

650 # noinspection PyTypeChecker 

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

652 

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

654 

655 def test_deserialize_invalid_json_fails_validation(self) -> None: 

656 widget = JsonWidget(self.request) 

657 

658 pstruct = "{" 

659 

660 with self.assertRaises(Invalid) as cm: 

661 # noinspection PyTypeChecker 

662 widget.deserialize(None, pstruct) 

663 

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

665 

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

667 

668 

669class JsonTypeTests(TestCase): 

670 def test_deserialize_valid_json(self) -> None: 

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

672 

673 json_type = JsonType() 

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

675 self.assertEqual(json_value, original) 

676 

677 def test_deserialize_null_returns_none(self) -> None: 

678 json_type = JsonType() 

679 json_value = json_type.deserialize(None, null) 

680 self.assertIsNone(json_value) 

681 

682 def test_deserialize_none_returns_null(self) -> None: 

683 json_type = JsonType() 

684 json_value = json_type.deserialize(None, None) 

685 self.assertIsNone(json_value) 

686 

687 def test_deserialize_invalid_json_returns_none(self) -> None: 

688 json_type = JsonType() 

689 json_value = json_type.deserialize(None, "{") 

690 self.assertIsNone(json_value) 

691 

692 def test_serialize_valid_appstruct(self) -> None: 

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

694 

695 json_type = JsonType() 

696 json_string = json_type.serialize(None, original) 

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

698 

699 def test_serialize_null_returns_null(self) -> None: 

700 json_type = JsonType() 

701 json_string = json_type.serialize(None, null) 

702 self.assertIs(json_string, null) 

703 

704 

705class TaskScheduleNodeTests(TestCase): 

706 def test_deserialize_not_a_json_object_fails_validation(self) -> None: 

707 node = TaskScheduleNode() 

708 with self.assertRaises(Invalid) as cm: 

709 node.deserialize({}) 

710 

711 self.assertIn( 

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

713 ) 

714 

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

716 

717 

718class TaskScheduleSelectorTests(BasicDatabaseTestCase): 

719 def test_displays_only_users_schedules(self) -> None: 

720 user = self.create_user(username="regular_user") 

721 my_group = self.create_group("mygroup") 

722 not_my_group = self.create_group("notmygroup") 

723 self.dbsession.flush() 

724 

725 self.create_membership(user, my_group, may_manage_patients=True) 

726 

727 my_schedule = TaskSchedule() 

728 my_schedule.group_id = my_group.id 

729 my_schedule.name = "My group's schedule" 

730 self.dbsession.add(my_schedule) 

731 

732 not_my_schedule = TaskSchedule() 

733 not_my_schedule.group_id = not_my_group.id 

734 not_my_schedule.name = "Not my group's schedule" 

735 self.dbsession.add(not_my_schedule) 

736 self.dbsession.commit() 

737 

738 self.req._debugging_user = user 

739 

740 selector = TaskScheduleSelector().bind(request=self.req) 

741 self.assertIn( 

742 (my_schedule.id, my_schedule.name), selector.widget.values 

743 ) 

744 self.assertNotIn( 

745 (not_my_schedule.id, not_my_schedule.name), selector.widget.values 

746 ) 

747 

748 

749class GroupIpUseWidgetTests(TestCase): 

750 def setUp(self) -> None: 

751 super().setUp() 

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

753 

754 def test_serialize_renders_template_with_values(self) -> None: 

755 widget = GroupIpUseWidget(self.request) 

756 

757 field = mock.Mock() 

758 field.renderer = mock.Mock() 

759 

760 cstruct = { 

761 IpContexts.CLINICAL: False, 

762 IpContexts.COMMERCIAL: False, 

763 IpContexts.EDUCATIONAL: True, 

764 IpContexts.RESEARCH: True, 

765 } 

766 

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

768 

769 args, kwargs = field.renderer.call_args 

770 

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

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

773 

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

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

776 self.assertTrue(kwargs[IpContexts.EDUCATIONAL]) 

777 self.assertTrue(kwargs[IpContexts.RESEARCH]) 

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

779 

780 def test_serialize_renders_readonly_template(self) -> None: 

781 widget = GroupIpUseWidget(self.request) 

782 

783 field = mock.Mock() 

784 field.renderer = mock.Mock() 

785 

786 cstruct = { 

787 IpContexts.CLINICAL: False, 

788 IpContexts.COMMERCIAL: False, 

789 IpContexts.EDUCATIONAL: True, 

790 IpContexts.RESEARCH: True, 

791 } 

792 

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

794 

795 args, kwargs = field.renderer.call_args 

796 

797 self.assertEqual( 

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

799 ) 

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

801 

802 def test_serialize_readonly_widget_renders_readonly_template(self) -> None: 

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

804 

805 field = mock.Mock() 

806 field.renderer = mock.Mock() 

807 

808 cstruct = { 

809 IpContexts.CLINICAL: False, 

810 IpContexts.COMMERCIAL: False, 

811 IpContexts.EDUCATIONAL: True, 

812 IpContexts.RESEARCH: True, 

813 } 

814 

815 widget.serialize(field, cstruct) 

816 

817 args, kwargs = field.renderer.call_args 

818 

819 self.assertEqual( 

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

821 ) 

822 

823 def test_serialize_with_null_defaults_to_false_values(self) -> None: 

824 widget = GroupIpUseWidget(self.request) 

825 

826 field = mock.Mock() 

827 field.renderer = mock.Mock() 

828 

829 widget.serialize(field, null) 

830 

831 args, kwargs = field.renderer.call_args 

832 

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

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

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

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

837 

838 def test_serialize_with_none_defaults_to_false_values(self) -> None: 

839 widget = GroupIpUseWidget(self.request) 

840 

841 field = mock.Mock() 

842 field.renderer = mock.Mock() 

843 

844 widget.serialize(field, None) 

845 

846 args, kwargs = field.renderer.call_args 

847 

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

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

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

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

852 

853 def test_deserialize_with_null_defaults_to_false_values(self) -> None: 

854 widget = GroupIpUseWidget(self.request) 

855 

856 field = None # Not used 

857 # noinspection PyTypeChecker 

858 cstruct = widget.deserialize(field, null) 

859 

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

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

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

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

864 

865 def test_deserialize_converts_to_bool_values(self) -> None: 

866 widget = GroupIpUseWidget(self.request) 

867 

868 field = None # Not used 

869 

870 # It shouldn't matter what the values are set to so long as the keys 

871 # are present. In practice the values will be set to "1" 

872 pstruct = {IpContexts.EDUCATIONAL: "1", IpContexts.RESEARCH: "1"} 

873 

874 # noinspection PyTypeChecker 

875 cstruct = widget.deserialize(field, pstruct) 

876 

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

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

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

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

881 

882 

883class IpUseTypeTests(TestCase): 

884 def test_deserialize_none_returns_none(self) -> None: 

885 ip_use_type = IpUseType() 

886 

887 node = None # not used 

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

889 

890 def test_deserialize_null_returns_none(self) -> None: 

891 ip_use_type = IpUseType() 

892 

893 node = None # not used 

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

895 

896 def test_deserialize_returns_ip_use_object(self) -> None: 

897 ip_use_type = IpUseType() 

898 

899 node = None # not used 

900 

901 cstruct = { 

902 IpContexts.CLINICAL: False, 

903 IpContexts.COMMERCIAL: True, 

904 IpContexts.EDUCATIONAL: False, 

905 IpContexts.RESEARCH: True, 

906 } 

907 ip_use = ip_use_type.deserialize(node, cstruct) 

908 

909 self.assertFalse(ip_use.clinical) 

910 self.assertTrue(ip_use.commercial) 

911 self.assertFalse(ip_use.educational) 

912 self.assertTrue(ip_use.research) 

913 

914 

915class MfaSecretWidgetTests(TestCase): 

916 def setUp(self) -> None: 

917 super().setUp() 

918 self.request = mock.Mock( 

919 gettext=lambda t: t, user=mock.Mock(username="test") 

920 ) 

921 self.mfa_secret = "HVIHV7TUFQPV7KAIJE2GSJTLTEAQIQSJ" 

922 

923 def test_serialize_renders_template_with_values(self) -> None: 

924 widget = MfaSecretWidget(self.request) 

925 

926 field = mock.Mock() 

927 field.renderer = mock.Mock() 

928 

929 cstruct = self.mfa_secret 

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

931 

932 args, kwargs = field.renderer.call_args 

933 

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

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

936 

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

938 

939 def test_serialize_renders_readonly_template(self) -> None: 

940 widget = MfaSecretWidget(self.request) 

941 

942 field = mock.Mock() 

943 field.renderer = mock.Mock() 

944 

945 cstruct = self.mfa_secret 

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

947 

948 args, kwargs = field.renderer.call_args 

949 

950 self.assertEqual( 

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

952 ) 

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

954 

955 def test_serialize_readonly_widget_renders_readonly_template(self) -> None: 

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

957 

958 field = mock.Mock() 

959 field.renderer = mock.Mock() 

960 

961 cstruct = self.mfa_secret 

962 widget.serialize(field, cstruct) 

963 

964 args, kwargs = field.renderer.call_args 

965 

966 self.assertEqual( 

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

968 ) 

969 

970 

971class PhoneNumberTypeTestCase(TestCase): 

972 def setUp(self) -> None: 

973 super().setUp() 

974 

975 self.request = mock.Mock() 

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

977 self.node = mock.Mock() 

978 

979 

980class PhoneNumberTypeDeserializeTests(PhoneNumberTypeTestCase): 

981 def test_returns_null_for_null_cstruct(self) -> None: 

982 # For allow_empty=True: 

983 phone_number = self.phone_type.deserialize(self.node, null) 

984 self.assertIs(phone_number, null) 

985 

986 def test_raises_for_unparsable_number(self) -> None: 

987 with self.assertRaises(Invalid) as cm: 

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

989 

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

991 

992 def test_raises_for_invalid_parsable_number(self) -> None: 

993 with self.assertRaises(Invalid) as cm: 

994 self.phone_type.deserialize(self.node, "+4411349600") 

995 

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

997 

998 def test_returns_valid_phone_number(self) -> None: 

999 phone_number = self.phone_type.deserialize( 

1000 self.node, TEST_PHONE_NUMBER 

1001 ) 

1002 

1003 self.assertIsInstance(phone_number, phonenumbers.PhoneNumber) 

1004 

1005 self.assertEqual( 

1006 phonenumbers.format_number( 

1007 phone_number, phonenumbers.PhoneNumberFormat.E164 

1008 ), 

1009 TEST_PHONE_NUMBER, 

1010 ) 

1011 

1012 

1013class PhoneNumberTypeSerializeTests(PhoneNumberTypeTestCase): 

1014 def test_returns_null_for_appstruct_none(self) -> None: 

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

1016 

1017 def test_returns_number_formatted_e164(self) -> None: 

1018 phone_number = phonenumbers.parse(TEST_PHONE_NUMBER) 

1019 

1020 self.assertEqual( 

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

1022 TEST_PHONE_NUMBER, 

1023 ) 

1024 

1025 

1026class PhoneNumberTypeMandatoryTestCase(TestCase): 

1027 def setUp(self) -> None: 

1028 super().setUp() 

1029 

1030 self.request = mock.Mock() 

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

1032 self.node = mock.Mock() 

1033 

1034 

1035class PhoneNumberTypeMandatoryDeserializeTests( 

1036 PhoneNumberTypeMandatoryTestCase 

1037): 

1038 def test_raises_for_appstruct_none(self) -> None: 

1039 # For allow_empty=False: 

1040 with self.assertRaises(Invalid): 

1041 self.phone_type.deserialize(self.node, null)