Coverage for tests / test_derivepassphrase_cli / test_vault_cli_notes_handling.py: 100.000%

191 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 21:34 +0200

1# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info> 

2# 

3# SPDX-License-Identifier: Zlib 

4 

5"""Tests for the `derivepassphrase vault` command-line interface: notes handling.""" 

6 

7from __future__ import annotations 

8 

9import contextlib 

10import json 

11import types 

12from typing import TYPE_CHECKING 

13 

14import click.testing 

15import hypothesis 

16import pytest 

17from hypothesis import strategies 

18from typing_extensions import Any, TypedDict 

19 

20from derivepassphrase import _types, cli 

21from derivepassphrase._internals import ( 

22 cli_helpers, 

23 cli_messages, 

24) 

25from tests import data, machinery 

26from tests.machinery import pytest as pytest_machinery 

27 

28if TYPE_CHECKING: 

29 from typing import NoReturn 

30 

31 from typing_extensions import Literal, NotRequired 

32 

33DUMMY_SERVICE = data.DUMMY_SERVICE 

34DUMMY_PASSPHRASE = data.DUMMY_PASSPHRASE 

35DUMMY_CONFIG_SETTINGS = data.DUMMY_CONFIG_SETTINGS 

36DUMMY_RESULT_PASSPHRASE = data.DUMMY_RESULT_PASSPHRASE 

37 

38 

39def is_warning_line(line: str) -> bool: 

40 """Return true if the line is a warning line.""" 

41 return " Warning: " in line or " Deprecation warning: " in line 1fbc

42 

43 

44class Strategies: 

45 @staticmethod 

46 def notes(*, max_size: int = 512) -> strategies.SearchStrategy[str]: 

47 return strategies.text( 

48 strategies.characters( 

49 min_codepoint=32, max_codepoint=126, include_characters="\n" 

50 ), 

51 min_size=1, 

52 max_size=max_size, 

53 ) 

54 

55 

56class Parametrize(types.SimpleNamespace): 

57 """Common test parametrizations.""" 

58 

59 MODERN_EDITOR_INTERFACE = pytest.mark.parametrize( 

60 "modern_editor_interface", [False, True], ids=["legacy", "modern"] 

61 ) 

62 NOTES_PLACEMENT = pytest.mark.parametrize( 

63 ["notes_placement", "placement_args"], 

64 [ 

65 pytest.param("after", ["--print-notes-after"], id="after"), 

66 pytest.param("before", ["--print-notes-before"], id="before"), 

67 ], 

68 ) 

69 

70 

71class TestNotesPrinting: 

72 """Tests concerning printing the service notes.""" 

73 

74 def _test( 

75 self, 

76 notes: str, 

77 /, 

78 notes_placement: Literal["before", "after"] | None = None, 

79 placement_args: list[str] | tuple[str, ...] = (), 

80 ) -> None: 

81 notes_stripped = notes.strip() 1hg

82 maybe_notes = {"notes": notes_stripped} if notes_stripped else {} 1hg

83 vault_config = { 1hg

84 "global": {"phrase": DUMMY_PASSPHRASE}, 

85 "services": { 

86 DUMMY_SERVICE: {**maybe_notes, **DUMMY_CONFIG_SETTINGS} 

87 }, 

88 } 

89 result_phrase = DUMMY_RESULT_PASSPHRASE.decode("ascii") 1hg

90 expected = ( 1hg

91 f"{notes_stripped}\n\n{result_phrase}\n" 

92 if notes_placement == "before" 

93 else f"{result_phrase}\n\n{notes_stripped}\n\n" 

94 if notes_placement == "after" 

95 else None 

96 ) 

97 runner = machinery.CliRunner(mix_stderr=notes_placement is not None) 1hg

98 # TODO(the-13th-letter): Rewrite using parenthesized 

99 # with-statements. 

100 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 

101 with contextlib.ExitStack() as stack: 1hg

102 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1hg

103 stack.enter_context( 1hg

104 pytest_machinery.isolated_vault_config( 

105 monkeypatch=monkeypatch, 

106 runner=runner, 

107 vault_config=vault_config, 

108 ) 

109 ) 

110 result = runner.invoke( 1hg

111 cli.derivepassphrase_vault, 

112 [*placement_args, "--", DUMMY_SERVICE], 

113 catch_exceptions=False, 

114 ) 

115 if expected is not None: 1hg

116 assert result.clean_exit(output=expected), ( 1h

117 "expected clean exit" 

118 ) 

119 else: 

120 assert result.clean_exit(), "expected clean exit" 1g

121 assert result.stdout, "expected program output" 1g

122 assert result.stdout.strip() == result_phrase, ( 1g

123 "expected known program output" 

124 ) 

125 assert result.stderr or not notes_stripped, "expected stderr" 1g

126 assert "Error:" not in result.stderr, ( 1g

127 "expected no error messages on stderr" 

128 ) 

129 assert result.stderr.strip() == notes_stripped, ( 1hg

130 "expected known stderr contents" 

131 ) 

132 

133 @hypothesis.given(notes=Strategies.notes()) 

134 def test_service_with_notes_actually_prints_notes( 1ag

135 self, 

136 notes: str, 

137 ) -> None: 

138 """Service notes are printed, if they contain content. 

139 

140 (Service notes are trimmed before printing, and thus result in 

141 output if and only if they are non-empty after trimming leading 

142 and trailing whitespace.) 

143 

144 """ 

145 hypothesis.assume("Error:" not in notes) 1g

146 self._test(notes, notes_placement=None, placement_args=()) 1g

147 

148 @Parametrize.NOTES_PLACEMENT 

149 @hypothesis.given(notes=Strategies.notes().filter(str.strip)) 1ah

150 @hypothesis.example(notes=" ").xfail( 

151 reason="empty notes", raises=AssertionError 

152 ) 

153 def test_notes_placement( 

154 self, 

155 notes_placement: Literal["before", "after"], 

156 placement_args: list[str], 

157 notes: str, 

158 ) -> None: 

159 self._test( 1h

160 notes, 

161 notes_placement=notes_placement, 

162 placement_args=placement_args, 

163 ) 

164 

165 

166class TestNotesEditing: 

167 """Superclass for tests concerning editing service notes.""" 

168 

169 CURRENT_NOTES = "Contents go here" 

170 OLD_NOTES_TEXT = ( 

171 "These backup notes are left over from the previous session." 

172 ) 

173 

174 def _calculate_expected_contents( 

175 self, 

176 final_notes: str, 

177 /, 

178 *, 

179 modern_editor_interface: bool, 

180 current_notes: str | None = CURRENT_NOTES, 

181 old_notes_text: str | None = OLD_NOTES_TEXT, 

182 ) -> tuple[str, str]: 

183 current_notes = current_notes or "" 1ebdc

184 old_notes_text = old_notes_text or "" 1ebdc

185 # For the modern editor interface, the notes change if and only 

186 # if the notes change to a different, non-empty string. There 

187 # are no backup notes, so we return the old ones (which may be 

188 # synthetic) unchanged. 

189 if modern_editor_interface: 1ebdc

190 return old_notes_text.strip(), ( 1ebdc

191 final_notes.strip() 

192 if final_notes.strip() 

193 and final_notes.strip() != current_notes.strip() 

194 else current_notes.strip() 

195 ) 

196 # For the legacy editor interface, the notes and the backup 

197 # notes change if and only if the new notes differ from the 

198 # previous notes. 

199 return ( 1bdc

200 (current_notes.strip(), final_notes.strip()) 

201 if final_notes.strip() != current_notes.strip() 

202 else (old_notes_text.strip(), current_notes.strip()) 

203 ) 

204 

205 def _test( 

206 self, 

207 edit_result: str, 

208 /, 

209 *, 

210 modern_editor_interface: bool, 

211 current_notes: str | None = CURRENT_NOTES, 

212 old_notes_text: str | None = OLD_NOTES_TEXT, 

213 ) -> tuple[machinery.ReadableResult, str, _types.VaultConfig]: 

214 if hypothesis.currently_in_test_context(): # pragma: no branch 1ebdc

215 hypothesis.note(f"{edit_result = }") 1ebdc

216 hypothesis.note(f"{modern_editor_interface = }") 1ebdc

217 hypothesis.note( 1ebdc

218 f"vault_config = {self._vault_config(current_notes or '')}" 

219 ) 

220 runner = machinery.CliRunner(mix_stderr=False) 1ebdc

221 # TODO(the-13th-letter): Rewrite using parenthesized 

222 # with-statements. 

223 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 

224 with contextlib.ExitStack() as stack: 1ebdc

225 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1ebdc

226 stack.enter_context( 1ebdc

227 pytest_machinery.isolated_vault_config( 

228 monkeypatch=monkeypatch, 

229 runner=runner, 

230 vault_config=self._vault_config(current_notes or ""), 

231 ) 

232 ) 

233 notes_backup_file = cli_helpers.config_filename( 1ebdc

234 subsystem="notes backup" 

235 ) 

236 if old_notes_text and old_notes_text.strip(): # pragma: no branch 1ebdc

237 notes_backup_file.write_text( 1ebdc

238 old_notes_text.strip(), encoding="UTF-8" 

239 ) 

240 monkeypatch.setattr(click, "edit", lambda *_a, **_kw: edit_result) 1ebdc

241 result = runner.invoke( 1ebdc

242 cli.derivepassphrase_vault, 

243 [ 

244 "--config", 

245 "--notes", 

246 "--modern-editor-interface" 

247 if modern_editor_interface 

248 else "--vault-legacy-editor-interface", 

249 "--", 

250 "sv", 

251 ], 

252 catch_exceptions=False, 

253 ) 

254 backup_contents = notes_backup_file.read_text(encoding="UTF-8") 1ebdc

255 with cli_helpers.config_filename(subsystem="vault").open( 1ebdc

256 encoding="UTF-8" 

257 ) as infile: 

258 config = json.load(infile) 1ebdc

259 if hypothesis.currently_in_test_context(): # pragma: no branch 1ebdc

260 hypothesis.note(f"{result = }") 1ebdc

261 hypothesis.note(f"{backup_contents = }") 1ebdc

262 hypothesis.note(f"{config = }") 1ebdc

263 return result, backup_contents, config 1ebdc

264 

265 def _assert_noop_exit( 

266 self, 

267 result: machinery.ReadableResult, 

268 /, 

269 *, 

270 modern_editor_interface: bool = False, 

271 ) -> None: 

272 # We do not distinguish between aborts and no-op edits. Aborts 

273 # are treated as failures (error exit), and thus tested 

274 # specifically in a different class. 

275 if modern_editor_interface: 1d

276 assert result.error_exit( 1d

277 error="the user aborted the request" 

278 ) or result.clean_exit(empty_stderr=True), "expected clean exit" 

279 else: 

280 assert result.clean_exit(empty_stderr=False), "expected clean exit" 1d

281 

282 def _assert_normal_exit(self, result: machinery.ReadableResult) -> None: 

283 assert result.clean_exit(), "expected clean exit" 1bc

284 assert all(map(is_warning_line, result.stderr.splitlines(True))) 1bc

285 

286 def _assert_notes_backup_warning( 

287 self, 

288 caplog: pytest.LogCaptureFixture, 

289 /, 

290 *, 

291 modern_editor_interface: bool, 

292 notes_unchanged: bool = False, 

293 ) -> None: 

294 assert ( 1bdc

295 modern_editor_interface 

296 or notes_unchanged 

297 or machinery.warning_emitted( 

298 "A backup copy of the old notes was saved", 

299 caplog.record_tuples, 

300 ) 

301 ), "expected known warning message on stderr" 

302 

303 def _assert_notes_and_backup_notes( 

304 self, 

305 /, 

306 *, 

307 final_notes: str, 

308 new_backup_notes: str, 

309 new_config: _types.VaultConfig, 

310 modern_editor_interface: bool, 

311 current_notes: str | None = CURRENT_NOTES, 

312 old_notes_text: str | None = OLD_NOTES_TEXT, 

313 ) -> None: 

314 if hypothesis.currently_in_test_context(): # pragma: no branch 1ebdc

315 hypothesis.note(f"{final_notes = }") 1ebdc

316 hypothesis.note(f"{current_notes = }") 1ebdc

317 expected_backup_notes, expected_notes = ( 1ebdc

318 self._calculate_expected_contents( 

319 final_notes, 

320 modern_editor_interface=modern_editor_interface, 

321 current_notes=current_notes, 

322 old_notes_text=old_notes_text, 

323 ) 

324 ) 

325 expected_config = self._vault_config(expected_notes) 1ebdc

326 assert new_config == expected_config 1ebdc

327 assert new_backup_notes == expected_backup_notes 1ebdc

328 

329 @staticmethod 

330 def _vault_config( 

331 starting_notes: str = CURRENT_NOTES, / 

332 ) -> _types.VaultConfig: 

333 return { 1ebdc

334 "global": {"phrase": "abc"}, 

335 "services": { 

336 "sv": {"notes": starting_notes.strip()} 

337 if starting_notes.strip() 

338 else {} 

339 }, 

340 } 

341 

342 class ExtraArgs(TypedDict): 

343 modern_editor_interface: bool 

344 current_notes: NotRequired[str] 

345 old_notes_text: NotRequired[str] 

346 

347 

348class TestNotesEditingValid(TestNotesEditing): 

349 """Tests concerning editing service notes: valid calls.""" 

350 

351 @Parametrize.MODERN_EDITOR_INTERFACE 

352 @hypothesis.settings( 1ac

353 suppress_health_check=[ 

354 *hypothesis.settings().suppress_health_check, 

355 hypothesis.HealthCheck.function_scoped_fixture, 

356 ], 

357 ) 

358 @hypothesis.given( 

359 notes=Strategies 

360 .notes() 

361 .filter(str.strip) 

362 .filter(lambda notes: notes != TestNotesEditingValid.CURRENT_NOTES) 

363 ) 

364 @hypothesis.example(TestNotesEditing.CURRENT_NOTES) 

365 @hypothesis.example(notes=" ").xfail( 

366 reason="empty notes", raises=AssertionError 

367 ) 

368 def test_successful_edit( 

369 self, 

370 caplog: pytest.LogCaptureFixture, 

371 modern_editor_interface: bool, 

372 notes: str, 

373 ) -> None: 

374 """Editing notes works.""" 

375 # Reset caplog between hypothesis runs. 

376 caplog.clear() 1c

377 marker = cli_messages.TranslatedString( 1c

378 cli_messages.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER 

379 ) 

380 edit_result = ( 1c

381 f""" 

382 

383{marker} 

384{notes} 

385""" 

386 if modern_editor_interface 

387 else notes.strip() 

388 ) 

389 

390 extra_args: TestNotesEditing.ExtraArgs = { 1c

391 "modern_editor_interface": modern_editor_interface, 

392 "current_notes": self.CURRENT_NOTES, 

393 "old_notes_text": self.OLD_NOTES_TEXT, 

394 } 

395 notes_unchanged = notes.strip() == extra_args["current_notes"].strip() 1c

396 

397 result, new_backup_notes, new_config = self._test( 1c

398 edit_result, **extra_args 

399 ) 

400 self._assert_normal_exit(result) 1c

401 self._assert_notes_and_backup_notes( 1c

402 final_notes=notes, 

403 new_backup_notes=new_backup_notes, 

404 new_config=new_config, 

405 **extra_args, 

406 ) 

407 self._assert_notes_backup_warning( 1c

408 caplog, 

409 modern_editor_interface=modern_editor_interface, 

410 notes_unchanged=notes_unchanged, 

411 ) 

412 

413 # TODO(the-13th-letter): Investigate behavior with respect to 

414 # whitespace-only notes and the legacy editor interface. 

415 @Parametrize.MODERN_EDITOR_INTERFACE 

416 @hypothesis.settings( 1ad

417 suppress_health_check=[ 

418 *hypothesis.settings().suppress_health_check, 

419 hypothesis.HealthCheck.function_scoped_fixture, 

420 ], 

421 ) 

422 @hypothesis.given(notes=Strategies.notes().filter(str.strip)) 

423 @hypothesis.example(TestNotesEditing.CURRENT_NOTES) 

424 def test_noop_edit( 

425 self, 

426 caplog: pytest.LogCaptureFixture, 

427 modern_editor_interface: bool, 

428 notes: str, 

429 ) -> None: 

430 """No-op editing existing notes works. 

431 

432 The notes are unchanged, and the command-line interface does not 

433 report an abort. For the legacy editor interface, the backup 

434 notes are unchanged as well. 

435 

436 """ 

437 # Reset caplog between hypothesis runs. 

438 caplog.clear() 1d

439 marker = cli_messages.TranslatedString( 1d

440 cli_messages.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER 

441 ) 

442 edit_result = (f"{marker}\n" if modern_editor_interface else "") + ( 1d

443 " " * 6 + notes + "\n" * 6 

444 ) 

445 

446 extra_args: TestNotesEditing.ExtraArgs = { 1d

447 "modern_editor_interface": modern_editor_interface, 

448 "current_notes": notes.strip(), 

449 "old_notes_text": self.OLD_NOTES_TEXT, 

450 } 

451 

452 result, new_backup_notes, new_config = self._test( 1d

453 edit_result, **extra_args 

454 ) 

455 self._assert_noop_exit( 1d

456 result, 

457 modern_editor_interface=modern_editor_interface, 

458 ) 

459 self._assert_notes_and_backup_notes( 1d

460 final_notes=notes, 

461 new_backup_notes=new_backup_notes, 

462 new_config=new_config, 

463 **extra_args, 

464 ) 

465 self._assert_notes_backup_warning( 1d

466 caplog, 

467 modern_editor_interface=modern_editor_interface, 

468 notes_unchanged=True, 

469 ) 

470 

471 # TODO(the-13th-letter): Keep this behavior or not, with or without 

472 # warning? 

473 @Parametrize.MODERN_EDITOR_INTERFACE 

474 @hypothesis.settings( 1ab

475 suppress_health_check=[ 

476 *hypothesis.settings().suppress_health_check, 

477 hypothesis.HealthCheck.function_scoped_fixture, 

478 ], 

479 ) 

480 @hypothesis.given(notes=Strategies.notes().filter(str.strip)) 

481 @hypothesis.example(notes=" ").xfail( 

482 reason="empty notes", raises=AssertionError 

483 ) 

484 def test_marker_removed( 

485 self, 

486 caplog: pytest.LogCaptureFixture, 

487 modern_editor_interface: bool, 

488 notes: str, 

489 ) -> None: 

490 """Removing the notes marker still saves the notes. 

491 

492 TODO: Keep this behavior or not, with or without warning? 

493 

494 """ 

495 notes_marker = cli_messages.TranslatedString( 1b

496 cli_messages.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER 

497 ) 

498 hypothesis.assume(str(notes_marker) not in notes.strip()) 1b

499 # Reset caplog between hypothesis runs. 

500 caplog.clear() 1b

501 

502 extra_args: TestNotesEditing.ExtraArgs = { 1b

503 "modern_editor_interface": modern_editor_interface, 

504 "current_notes": self.CURRENT_NOTES, 

505 "old_notes_text": self.OLD_NOTES_TEXT, 

506 } 

507 notes_unchanged = notes.strip() == extra_args["current_notes"].strip() 1b

508 

509 result, new_backup_notes, new_config = self._test( 1b

510 notes.strip(), **extra_args 

511 ) 

512 self._assert_normal_exit(result) 1b

513 self._assert_notes_and_backup_notes( 1b

514 final_notes=notes, 

515 new_backup_notes=new_backup_notes, 

516 new_config=new_config, 

517 **extra_args, 

518 ) 

519 self._assert_notes_backup_warning( 1b

520 caplog, 

521 modern_editor_interface=modern_editor_interface, 

522 notes_unchanged=notes_unchanged, 

523 ) 

524 

525 

526class TestNotesEditingInvalid(TestNotesEditing): 

527 """Tests concerning editing service notes: invalid/error calls.""" 

528 

529 @hypothesis.given(notes=Strategies.notes()) 

530 @hypothesis.example(notes="") 1ae

531 def test_abort( 

532 self, 

533 notes: str, 

534 ) -> None: 

535 """Aborting editing notes works, even if no notes are stored yet. 

536 

537 Aborting is only supported with the modern editor interface. 

538 

539 """ 

540 edit_result = "" 1e

541 

542 extra_args: TestNotesEditing.ExtraArgs = { 1e

543 "modern_editor_interface": True, 

544 "current_notes": notes.strip(), 

545 "old_notes_text": self.OLD_NOTES_TEXT, 

546 } 

547 

548 result, new_backup_notes, new_config = self._test( 1e

549 edit_result, **extra_args 

550 ) 

551 assert result.error_exit(error="the user aborted the request"), ( 1e

552 "expected error exit" 

553 ) 

554 self._assert_notes_and_backup_notes( 1e

555 final_notes=notes.strip(), 

556 new_backup_notes=new_backup_notes, 

557 new_config=new_config, 

558 **extra_args, 

559 ) 

560 

561 @Parametrize.MODERN_EDITOR_INTERFACE 

562 @hypothesis.settings( 1af

563 suppress_health_check=[ 

564 *hypothesis.settings().suppress_health_check, 

565 hypothesis.HealthCheck.function_scoped_fixture, 

566 ], 

567 ) 

568 @hypothesis.given(notes=Strategies.notes()) 

569 @hypothesis.example(notes="") 

570 def test_fail_on_config_option_missing( 

571 self, 

572 caplog: pytest.LogCaptureFixture, 

573 modern_editor_interface: bool, 

574 notes: str, 

575 ) -> None: 

576 """Editing notes fails (and warns) if `--config` is missing.""" 

577 maybe_notes = {"notes": notes.strip()} if notes.strip() else {} 1f

578 vault_config = { 1f

579 "global": {"phrase": DUMMY_PASSPHRASE}, 

580 "services": { 

581 DUMMY_SERVICE: {**maybe_notes, **DUMMY_CONFIG_SETTINGS} 

582 }, 

583 } 

584 old_notes_text = ( 1f

585 "These backup notes are left over from the previous session." 

586 ) 

587 # Reset caplog between hypothesis runs. 

588 caplog.clear() 1f

589 runner = machinery.CliRunner(mix_stderr=False) 1f

590 # TODO(the-13th-letter): Rewrite using parenthesized 

591 # with-statements. 

592 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9 

593 with contextlib.ExitStack() as stack: 1f

594 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1f

595 stack.enter_context( 1f

596 pytest_machinery.isolated_vault_config( 

597 monkeypatch=monkeypatch, 

598 runner=runner, 

599 vault_config=vault_config, 

600 ) 

601 ) 

602 EDIT_ATTEMPTED = "edit attempted!" # noqa: N806 1f

603 

604 def raiser(*_args: Any, **_kwargs: Any) -> NoReturn: 1f

605 pytest.fail(EDIT_ATTEMPTED) 

606 

607 notes_backup_file = cli_helpers.config_filename( 1f

608 subsystem="notes backup" 

609 ) 

610 notes_backup_file.write_text(old_notes_text, encoding="UTF-8") 1f

611 monkeypatch.setattr(click, "edit", raiser) 1f

612 result = runner.invoke( 1f

613 cli.derivepassphrase_vault, 

614 [ 

615 "--notes", 

616 "--modern-editor-interface" 

617 if modern_editor_interface 

618 else "--vault-legacy-editor-interface", 

619 "--", 

620 DUMMY_SERVICE, 

621 ], 

622 catch_exceptions=False, 

623 ) 

624 assert result.clean_exit( 1f

625 output=DUMMY_RESULT_PASSPHRASE.decode("ascii") 

626 ), "expected clean exit" 

627 assert result.stderr 1f

628 assert notes.strip() in result.stderr 1f

629 assert all( 1f

630 is_warning_line(line) 

631 for line in result.stderr.splitlines(True) 

632 if line.startswith(f"{cli.PROG_NAME}: ") 

633 ) 

634 assert machinery.warning_emitted( 1f

635 "Specifying --notes without --config is ineffective. " 

636 "No notes will be edited.", 

637 caplog.record_tuples, 

638 ), "expected known warning message in stderr" 

639 assert ( 1f

640 modern_editor_interface 

641 or notes_backup_file.read_text(encoding="UTF-8") 

642 == old_notes_text 

643 ) 

644 with cli_helpers.config_filename(subsystem="vault").open( 1f

645 encoding="UTF-8" 

646 ) as infile: 

647 config = json.load(infile) 1f

648 assert config == vault_config 1f