Coverage for tests / test_derivepassphrase_cli_export_vault.py: 100.000%

194 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"""Test the `derivepassphrase export vault` CLI.""" 

6 

7from __future__ import annotations 

8 

9import base64 

10import contextlib 

11import json 

12import pathlib 

13import types 

14from typing import TYPE_CHECKING 

15 

16import hypothesis 

17import pytest 

18from hypothesis import strategies 

19 

20from derivepassphrase import _types, cli, exporter 

21from derivepassphrase.exporter import storeroom, vault_native 

22from tests import data, machinery 

23from tests.machinery import pytest as pytest_machinery 

24 

25cryptography = pytest.importorskip("cryptography", minversion="38.0") 

26 

27from cryptography.hazmat.primitives import ( # noqa: E402 

28 ciphers, 

29 hashes, 

30 hmac, 

31 padding, 

32) 

33from cryptography.hazmat.primitives.ciphers import ( # noqa: E402 

34 algorithms, 

35 modes, 

36) 

37 

38if TYPE_CHECKING: 

39 from collections.abc import Callable, Generator 

40 from typing import Any 

41 

42 from typing_extensions import Buffer, Literal 

43 

44 

45class Parametrize(types.SimpleNamespace): 

46 BAD_CONFIG = pytest.mark.parametrize( 

47 "config", ["xxx", "null", '{"version": 255}'] 

48 ) 

49 VAULT_NATIVE_CONFIG_DATA = pytest.mark.parametrize( 

50 ["config", "format", "config_data"], 

51 [ 

52 pytest.param( 

53 data.VAULT_V02_CONFIG, 

54 "v0.2", 

55 data.VAULT_V02_CONFIG_DATA, 

56 id="V02_CONFIG-v0.2", 

57 ), 

58 pytest.param( 

59 data.VAULT_V02_CONFIG, 

60 "v0.3", 

61 exporter.NotAVaultConfigError, 

62 id="V02_CONFIG-v0.3", 

63 ), 

64 pytest.param( 

65 data.VAULT_V03_CONFIG, 

66 "v0.2", 

67 exporter.NotAVaultConfigError, 

68 id="V03_CONFIG-v0.2", 

69 ), 

70 pytest.param( 

71 data.VAULT_V03_CONFIG, 

72 "v0.3", 

73 data.VAULT_V03_CONFIG_DATA, 

74 id="V03_CONFIG-v0.3", 

75 ), 

76 ], 

77 ) 

78 VAULT_NATIVE_PARSER_CLASS_DATA = pytest.mark.parametrize( 

79 ["config", "parser_class", "config_data"], 

80 [ 

81 pytest.param( 

82 data.VAULT_V02_CONFIG, 

83 vault_native.VaultNativeV02ConfigParser, 

84 data.VAULT_V02_CONFIG_DATA, 

85 id="0.2", 

86 ), 

87 pytest.param( 

88 data.VAULT_V03_CONFIG, 

89 vault_native.VaultNativeV03ConfigParser, 

90 data.VAULT_V03_CONFIG_DATA, 

91 id="0.3", 

92 ), 

93 ], 

94 ) 

95 STOREROOM_HANDLER = pytest.mark.parametrize( 

96 "handler", 

97 [ 

98 pytest.param(storeroom.export_storeroom_data, id="handler"), 

99 pytest.param(exporter.export_vault_config_data, id="dispatcher"), 

100 ], 

101 ) 

102 VAULT_NATIVE_HANDLER = pytest.mark.parametrize( 

103 "handler", 

104 [ 

105 pytest.param(vault_native.export_vault_native_data, id="handler"), 

106 pytest.param(exporter.export_vault_config_data, id="dispatcher"), 

107 ], 

108 ) 

109 VAULT_NATIVE_PBKDF2_RESULT = pytest.mark.parametrize( 

110 ["iterations", "result"], 

111 [ 

112 pytest.param(100, b"6ede361e81e9c061efcdd68aeb768b80", id="100"), 

113 pytest.param(200, b"bcc7d01e075b9ffb69e702bf701187c1", id="200"), 

114 ], 

115 ) 

116 KEY_FORMATS = pytest.mark.parametrize( 

117 "key", 

118 [ 

119 None, 

120 pytest.param(data.VAULT_MASTER_KEY, id="str"), 

121 pytest.param(data.VAULT_MASTER_KEY.encode("ascii"), id="bytes"), 

122 pytest.param( 

123 bytearray(data.VAULT_MASTER_KEY.encode("ascii")), 

124 id="bytearray", 

125 ), 

126 pytest.param( 

127 memoryview(data.VAULT_MASTER_KEY.encode("ascii")), 

128 id="memoryview", 

129 ), 

130 ], 

131 ) 

132 BAD_MASTER_KEYS_DATA = pytest.mark.parametrize( 

133 ["master_keys_data", "err_msg"], 

134 [ 

135 pytest.param( 

136 '{"version": 255}', 

137 "bad or unsupported keys version header", 

138 id="v255", 

139 ), 

140 pytest.param( 

141 '{"version": 1}\nAAAA\nAAAA', 

142 "trailing data; cannot make sense", 

143 id="trailing-data", 

144 ), 

145 pytest.param( 

146 '{"version": 1}\nAAAA', 

147 "cannot handle version 0 encrypted keys", 

148 id="v0-keys", 

149 ), 

150 ], 

151 ) 

152 PATH = pytest.mark.parametrize("path", [".vault", None]) 

153 BAD_STOREROOM_CONFIG_DATA = pytest.mark.parametrize( 

154 ["zipped_config", "error_text"], 

155 [ 

156 pytest.param( 

157 data.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED, 

158 "Object key mismatch", 

159 id="VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED", 

160 ), 

161 pytest.param( 

162 data.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2, 

163 "Directory index is not actually an index", 

164 id="VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2", 

165 ), 

166 pytest.param( 

167 data.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3, 

168 "Directory index is not actually an index", 

169 id="VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3", 

170 ), 

171 pytest.param( 

172 data.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4, 

173 "Object key mismatch", 

174 id="VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4", 

175 ), 

176 ], 

177 ) 

178 

179 

180class TestCLIParameters: 

181 """Test the command-line interface for `derivepassphrase export vault`.""" 

182 

183 def _test( 

184 self, 

185 command_line: list[str], 

186 *, 

187 vault_config: str | bytes = data.VAULT_V03_CONFIG, 

188 config_data: dict[str, Any] = data.VAULT_V03_CONFIG_DATA, 

189 ) -> None: 

190 runner = machinery.CliRunner(mix_stderr=False) 1lmn

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

192 # with-statements. 

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

194 with contextlib.ExitStack() as stack: 1lmn

195 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1lmn

196 stack.enter_context( 1lmn

197 pytest_machinery.isolated_vault_exporter_config( 

198 monkeypatch=monkeypatch, 

199 runner=runner, 

200 vault_config=vault_config, 

201 vault_key=data.VAULT_MASTER_KEY, 

202 ) 

203 ) 

204 result = runner.invoke( 1lmn

205 cli.derivepassphrase_export_vault, 

206 command_line, 

207 ) 

208 assert result.clean_exit(empty_stderr=True), "expected clean exit" 1lmn

209 assert json.loads(result.stdout) == config_data 1lmn

210 

211 def test_path(self) -> None: 

212 """The path `VAULT_PATH` is supported. 

213 

214 Using `VAULT_PATH` as the path looks up the actual path in the 

215 `VAULT_PATH` environment variable. See 

216 [`exporter.get_vault_path`][] for details. 

217 

218 """ 

219 self._test(["VAULT_PATH"]) 1n

220 

221 def test_key(self) -> None: 

222 """The `--key` option is supported.""" 

223 self._test(["-k", data.VAULT_MASTER_KEY, ".vault"]) 1l

224 

225 @pytest_machinery.Parametrize.VAULT_CONFIG_FORMATS_DATA 

226 def test_load_vault_specific_format( 

227 self, 

228 config: str | bytes, 

229 format: str, 

230 config_data: dict[str, Any], 

231 ) -> None: 

232 """Passing a specific format works. 

233 

234 Passing a specific format name causes `derivepassphrase export 

235 vault` to only attempt decoding in that named format. 

236 

237 """ 

238 self._test( 1m

239 ["-f", format, "-k", data.VAULT_MASTER_KEY, "VAULT_PATH"], 

240 vault_config=config, 

241 config_data=config_data, 

242 ) 

243 

244 

245class TestCLIFailures: 

246 """Test the command-line interface for `derivepassphrase export vault`.""" 

247 

248 @contextlib.contextmanager 

249 def _test( 

250 self, 

251 caplog: pytest.LogCaptureFixture, 

252 command_line: list[str], 

253 /, 

254 error_message: str = "Cannot parse '.vault' " 

255 "as a valid vault-native config", 

256 *, 

257 vault_config: str | bytes = data.VAULT_V03_CONFIG, 

258 vault_key: str = data.VAULT_MASTER_KEY, 

259 ) -> Generator[pytest.MonkeyPatch, None, None]: 

260 runner = machinery.CliRunner(mix_stderr=False) 1ijdke

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

262 # with-statements. 

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

264 with contextlib.ExitStack() as stack: 1ijdke

265 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1ijdke

266 stack.enter_context( 1ijdke

267 pytest_machinery.isolated_vault_exporter_config( 

268 monkeypatch=monkeypatch, 

269 runner=runner, 

270 vault_config=vault_config, 

271 vault_key=vault_key, 

272 ) 

273 ) 

274 # If the setup blows up, bubble the exception. 

275 yield monkeypatch # noqa: RUF075 1ijdke

276 result = runner.invoke( 1ijdke

277 cli.derivepassphrase_export_vault, 

278 command_line, 

279 ) 

280 assert result.error_exit( 1ijdke

281 error=error_message, 

282 record_tuples=caplog.record_tuples, 

283 ), "expected error exit and known error message" 

284 assert data.CANNOT_LOAD_CRYPTOGRAPHY not in result.stderr 1ijdke

285 

286 def test_file_not_found( 

287 self, 

288 caplog: pytest.LogCaptureFixture, 

289 ) -> None: 

290 """Fail when trying to decode non-existant files/directories.""" 

291 with self._test( 1j

292 caplog, 

293 ["does-not-exist.txt"], 

294 error_message="Cannot parse 'does-not-exist.txt' " 

295 "as a valid vault-native config", 

296 ): 

297 pass 1j

298 

299 def test_invalid_encrypted_contents( 

300 self, 

301 caplog: pytest.LogCaptureFixture, 

302 ) -> None: 

303 """Fail to parse invalid vault configurations (files).""" 

304 with self._test(caplog, [".vault"], vault_config=""): 1k

305 pass 1k

306 

307 def test_just_a_directory( 

308 self, 

309 caplog: pytest.LogCaptureFixture, 

310 ) -> None: 

311 """Fail to parse invalid vault configurations (directories).""" 

312 with self._test(caplog, [".vault"], vault_config=""): 1e

313 p = pathlib.Path(".vault") 1e

314 p.unlink() 1e

315 p.mkdir() 1e

316 

317 def test_bad_signature( 

318 self, 

319 caplog: pytest.LogCaptureFixture, 

320 ) -> None: 

321 """Fail to parse vault configurations with invalid integrity checks.""" 

322 with self._test( 1i

323 caplog, 

324 ["-f", "v0.3", ".vault"], 

325 vault_config=data.VAULT_V02_CONFIG, 

326 ): 

327 pass 1i

328 

329 def test_invalid_decrypted_contents( 

330 self, 

331 caplog: pytest.LogCaptureFixture, 

332 ) -> None: 

333 """Fail to parse encrypted vault configurations with invalid plaintext.""" 

334 with self._test( 1d

335 caplog, [".vault"], error_message="Invalid vault config: " 

336 ) as monkeypatch: 

337 

338 def export_vault_config_data(*_args: Any, **_kwargs: Any) -> None: 1d

339 return None 1d

340 

341 monkeypatch.setattr( 1d

342 exporter, 

343 "export_vault_config_data", 

344 export_vault_config_data, 

345 ) 

346 

347 

348class TestStoreroom: 

349 """Test the "storeroom" handler and handler machinery.""" 

350 

351 @contextlib.contextmanager 

352 def _setup_environment( 

353 self, 

354 *, 

355 vault_config: str | bytes = data.VAULT_STOREROOM_CONFIG_ZIPPED, 

356 vault_key: str = data.VAULT_MASTER_KEY, 

357 ) -> Generator[tuple[pytest.MonkeyPatch, machinery.CliRunner], None, None]: 

358 runner = machinery.CliRunner(mix_stderr=False) 1fpog

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

360 # with-statements. 

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

362 with contextlib.ExitStack() as stack: 1fpog

363 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1fpog

364 stack.enter_context( 1fpog

365 pytest_machinery.isolated_vault_exporter_config( 

366 monkeypatch=monkeypatch, 

367 runner=runner, 

368 vault_config=vault_config, 

369 vault_key=vault_key, 

370 ) 

371 ) 

372 yield monkeypatch, runner 1fpog

373 

374 @Parametrize.PATH 

375 @Parametrize.KEY_FORMATS 

376 @Parametrize.STOREROOM_HANDLER 

377 def test_export_data_path_and_keys_type( 

378 self, 

379 path: str | None, 

380 key: str | Buffer | None, 

381 handler: exporter.ExportVaultConfigDataFunction, 

382 ) -> None: 

383 """Support different argument types. 

384 

385 The [`exporter.export_vault_config_data`][] dispatcher supports 

386 them as well. 

387 

388 """ 

389 with self._setup_environment(): 1p

390 assert ( 1p

391 handler(path, key, format="storeroom") 

392 == data.VAULT_STOREROOM_CONFIG_DATA 

393 ) 

394 

395 # TODO(the-13th-letter): Rewrite as property-based test. 

396 @pytest_machinery.correctness_focused 

397 def test_decrypt_bucket_item_unknown_version(self) -> None: 

398 """Fail on unknown versions of the master keys file.""" 

399 bucket_item = ( 1s

400 b"\xff" + bytes(storeroom.ENCRYPTED_KEYPAIR_SIZE) + bytes(3) 

401 ) 

402 master_keys = _types.StoreroomMasterKeys( 1s

403 encryption_key=bytes(storeroom.KEY_SIZE), 

404 signing_key=bytes(storeroom.KEY_SIZE), 

405 hashing_key=bytes(storeroom.KEY_SIZE), 

406 ) 

407 with pytest.raises(ValueError, match="Cannot handle version 255"): 1s

408 storeroom._decrypt_bucket_item(bucket_item, master_keys) 1s

409 

410 # TODO(the-13th-letter): Rewrite as property-based test. 

411 @pytest_machinery.correctness_focused 

412 @Parametrize.BAD_CONFIG 

413 def test_decrypt_bucket_file_bad_json_or_version( 

414 self, 

415 config: str, 

416 ) -> None: 

417 """Fail on bad or unsupported bucket file contents. 

418 

419 These include unknown versions, invalid JSON, or JSON of the 

420 wrong shape. 

421 

422 """ 

423 master_keys = _types.StoreroomMasterKeys( 1f

424 encryption_key=bytes(storeroom.KEY_SIZE), 

425 signing_key=bytes(storeroom.KEY_SIZE), 

426 hashing_key=bytes(storeroom.KEY_SIZE), 

427 ) 

428 with self._setup_environment(): 1f

429 p = pathlib.Path(".vault", "20") 1f

430 with p.open("w", encoding="UTF-8") as outfile: 1f

431 print(config, file=outfile) 1f

432 with pytest.raises(ValueError, match="Invalid bucket file: "): 1f

433 list(storeroom._decrypt_bucket_file(p, master_keys)) 1f

434 

435 # TODO(the-13th-letter): Rewrite as property-based test. 

436 @pytest_machinery.correctness_focused 

437 @Parametrize.BAD_MASTER_KEYS_DATA 

438 @Parametrize.STOREROOM_HANDLER 

439 def test_export_storeroom_data_bad_master_keys_file( 

440 self, 

441 master_keys_data: str, 

442 err_msg: str, 

443 handler: exporter.ExportVaultConfigDataFunction, 

444 ) -> None: 

445 """Fail on bad or unsupported master keys file contents. 

446 

447 These include unknown versions, and data of the wrong shape. 

448 

449 """ 

450 with self._setup_environment(): 1g

451 p = pathlib.Path(".vault", ".keys") 1g

452 with p.open("w", encoding="UTF-8") as outfile: 1g

453 print(master_keys_data, file=outfile) 1g

454 with pytest.raises(RuntimeError, match=err_msg): 1g

455 handler(format="storeroom") 1g

456 

457 # TODO(the-13th-letter): Rewrite as property-based test. 

458 @pytest_machinery.correctness_focused 

459 @Parametrize.BAD_STOREROOM_CONFIG_DATA 

460 @Parametrize.STOREROOM_HANDLER 

461 def test_export_storeroom_data_bad_directory_listing( 

462 self, 

463 zipped_config: bytes, 

464 error_text: str, 

465 handler: exporter.ExportVaultConfigDataFunction, 

466 ) -> None: 

467 """Fail on bad decoded directory structures. 

468 

469 If the decoded configuration contains directories whose 

470 structures are inconsistent, it detects this and fails: 

471 

472 - The key indicates a directory, but the contents don't. 

473 - The directory indicates children with invalid path names. 

474 - The directory indicates children that are missing from the 

475 configuration entirely. 

476 - The configuration contains nested subdirectories, but the 

477 higher-level directories don't indicate their 

478 subdirectories. 

479 

480 """ 

481 with self._setup_environment(vault_config=zipped_config): # noqa: SIM117 1o

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

483 # with-statements. 

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

485 with pytest.raises(RuntimeError, match=error_text): 1o

486 handler(format="storeroom") 1o

487 

488 # TODO(the-13th-letter): Rewrite as property-based test. 

489 @pytest_machinery.correctness_focused 

490 def test_decrypt_keys_wrong_data_length(self) -> None: 

491 """Fail on internal structural data of the wrong size. 

492 

493 Specifically, fail on internal structural data such as master 

494 keys or session keys that is correctly encrypted according to 

495 its MAC, but is of the wrong shape. (Since the data usually are 

496 keys and thus are opaque, the only detectable shape violation is 

497 the wrong size of the data.) 

498 

499 """ 

500 payload = ( 1b

501 b"Any text here, as long as it isn't exactly 64 or 96 bytes long." 

502 ) 

503 assert len(payload) not in frozenset({ 1b

504 2 * storeroom.KEY_SIZE, 

505 3 * storeroom.KEY_SIZE, 

506 }) 

507 key = b"DEADBEEFdeadbeefDeAdBeEfdEaDbEeF" 1b

508 padder = padding.PKCS7(storeroom.IV_SIZE * 8).padder() 1b

509 plaintext = bytearray(padder.update(payload)) 1b

510 plaintext.extend(padder.finalize()) 1b

511 iv = b"deadbeefDEADBEEF" 1b

512 assert len(iv) == storeroom.IV_SIZE 1b

513 encryptor = ciphers.Cipher( 1b

514 algorithms.AES256(key), modes.CBC(iv) 

515 ).encryptor() 

516 ciphertext = bytearray(encryptor.update(bytes(plaintext))) 1b

517 ciphertext.extend(encryptor.finalize()) 1b

518 mac_obj = hmac.HMAC(key, hashes.SHA256()) 1b

519 mac_obj.update(iv) 1b

520 mac_obj.update(bytes(ciphertext)) 1b

521 data = iv + bytes(ciphertext) + mac_obj.finalize() 1b

522 with pytest.raises( 1b

523 ValueError, 

524 match=r"Invalid encrypted master keys payload", 

525 ): 

526 storeroom._decrypt_master_keys_data( 1b

527 data, 

528 _types.StoreroomKeyPair(encryption_key=key, signing_key=key), 

529 ) 

530 with pytest.raises( 1b

531 ValueError, 

532 match=r"Invalid encrypted session keys payload", 

533 ): 

534 storeroom._decrypt_session_keys( 1b

535 data, 

536 _types.StoreroomMasterKeys( 

537 hashing_key=key, encryption_key=key, signing_key=key 

538 ), 

539 ) 

540 

541 # TODO(the-13th-letter): Generate key as well. 

542 @pytest_machinery.correctness_focused 

543 @hypothesis.given( 1ar

544 data=strategies.binary( 

545 min_size=storeroom.MAC_SIZE, max_size=storeroom.MAC_SIZE 

546 ), 

547 ) 

548 def test_decrypt_keys_invalid_signature(self, data: bytes) -> None: 

549 """Fail on bad MAC values.""" 

550 key = b"DEADBEEFdeadbeefDeAdBeEfdEaDbEeF" 1r

551 # Guessing a correct payload plus MAC would be a pre-image 

552 # attack on the underlying hash function (SHA-256), i.e. is 

553 # computationally infeasible, and the chance of finding one by 

554 # such random sampling is astronomically tiny. 

555 with pytest.raises(cryptography.exceptions.InvalidSignature): 1r

556 storeroom._decrypt_master_keys_data( 1r

557 data, 

558 _types.StoreroomKeyPair(encryption_key=key, signing_key=key), 

559 ) 

560 with pytest.raises(cryptography.exceptions.InvalidSignature): 1r

561 storeroom._decrypt_session_keys( 1r

562 data, 

563 _types.StoreroomMasterKeys( 

564 hashing_key=key, encryption_key=key, signing_key=key 

565 ), 

566 ) 

567 

568 

569class TestVaultNativeConfig: 

570 """Test the vault-native handler and handler machinery.""" 

571 

572 @contextlib.contextmanager 

573 def _setup_environment( 

574 self, 

575 *, 

576 vault_config: str | bytes = data.VAULT_V03_CONFIG, 

577 vault_key: str = data.VAULT_MASTER_KEY, 

578 ) -> Generator[tuple[pytest.MonkeyPatch, machinery.CliRunner], None, None]: 

579 runner = machinery.CliRunner(mix_stderr=False) 1qhc

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

581 # with-statements. 

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

583 with contextlib.ExitStack() as stack: 1qhc

584 monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) 1qhc

585 stack.enter_context( 1qhc

586 pytest_machinery.isolated_vault_exporter_config( 

587 monkeypatch=monkeypatch, 

588 runner=runner, 

589 vault_config=vault_config, 

590 vault_key=vault_key, 

591 ) 

592 ) 

593 yield monkeypatch, runner 1qhc

594 

595 @Parametrize.VAULT_NATIVE_PBKDF2_RESULT 

596 def test_pbkdf2_manually(self, iterations: int, result: bytes) -> None: 

597 """The PBKDF2 helper function works.""" 

598 assert ( 1u

599 vault_native.VaultNativeConfigParser._pbkdf2( 

600 data.VAULT_MASTER_KEY.encode("utf-8"), 32, iterations 

601 ) 

602 == result 

603 ) 

604 

605 @Parametrize.VAULT_NATIVE_CONFIG_DATA 

606 @Parametrize.VAULT_NATIVE_HANDLER 

607 def test_export_vault_native_data_explicit_version( 

608 self, 

609 config: str, 

610 format: Literal["v0.2", "v0.3"], 

611 config_data: _types.VaultConfig | type[Exception], 

612 handler: exporter.ExportVaultConfigDataFunction, 

613 ) -> None: 

614 """Accept data only of the correct version. 

615 

616 Note: Historic behavior 

617 `derivepassphrase` versions prior to 0.5 automatically tried 

618 to parse vault-native configurations as v0.3-type, then 

619 v0.2-type. Since `derivepassphrase` 0.5, the command-line 

620 interface still tries multi-version parsing, but the API 

621 no longer does. 

622 

623 """ 

624 with self._setup_environment(vault_config=config): 1h

625 if isinstance(config_data, type): 1h

626 with pytest.raises(config_data): 1h

627 handler(None, format=format) 1h

628 else: 

629 parsed_config = handler(None, format=format) 1h

630 assert parsed_config == config_data 1h

631 

632 @Parametrize.PATH 

633 @Parametrize.KEY_FORMATS 

634 @Parametrize.VAULT_NATIVE_HANDLER 

635 def test_export_data_path_and_keys_type( 

636 self, 

637 path: str | None, 

638 key: str | Buffer | None, 

639 handler: exporter.ExportVaultConfigDataFunction, 

640 ) -> None: 

641 """The handler supports different argument types. 

642 

643 The [`exporter.export_vault_config_data`][] dispatcher supports 

644 them as well. 

645 

646 """ 

647 with self._setup_environment(): 1q

648 assert ( 1q

649 handler(path, key, format="v0.3") == data.VAULT_V03_CONFIG_DATA 

650 ) 

651 

652 @Parametrize.VAULT_NATIVE_PARSER_CLASS_DATA 

653 def test_result_caching( 

654 self, 

655 config: str, 

656 parser_class: type[vault_native.VaultNativeConfigParser], 

657 config_data: dict[str, Any], 

658 ) -> None: 

659 """Cache the results of decrypting/decoding a configuration.""" 

660 

661 def null_func(name: str) -> Callable[..., None]: 1c

662 def func(*_args: Any, **_kwargs: Any) -> None: # pragma: no cover 1c

663 msg = f"disallowed and stubbed out function {name} called" 

664 raise AssertionError(msg) 

665 

666 return func 1c

667 

668 with self._setup_environment(vault_config=config) as values: 1c

669 monkeypatch, _ = values 1c

670 parser = parser_class( 1c

671 base64.b64decode(config), data.VAULT_MASTER_KEY 

672 ) 

673 assert parser() == config_data 1c

674 # Now stub out all functions used to calculate the above result. 

675 monkeypatch.setattr( 1c

676 parser, "_parse_contents", null_func("_parse_contents") 

677 ) 

678 monkeypatch.setattr( 1c

679 parser, "_derive_keys", null_func("_derive_keys") 

680 ) 

681 monkeypatch.setattr( 1c

682 parser, "_check_signature", null_func("_check_signature") 

683 ) 

684 monkeypatch.setattr( 1c

685 parser, "_decrypt_payload", null_func("_decrypt_payload") 

686 ) 

687 assert parser() == config_data 1c

688 super_call = vault_native.VaultNativeConfigParser.__call__ 1c

689 assert super_call(parser) == config_data 1c

690 

691 def test_no_password(self) -> None: 

692 """Fail on empty master keys/master passphrases.""" 

693 with pytest.raises(ValueError, match="Password must not be empty"): 1t

694 vault_native.VaultNativeV03ConfigParser(b"", b"") 1t