e8b3ecf264495b6e5cf9b5f07889545ed242b64b
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

1) # SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>
2) #
3) # SPDX-License-Identifier: MIT
4) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

5) from __future__ import annotations
6) 
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

7) from collections.abc import Callable
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

8) import json
9) import os
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

10) import socket
Marco Ricci Support Python 3.10 and PyP...

Marco Ricci authored 2 months ago

11) from typing_extensions import Any, cast, NamedTuple
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

12) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

13) import click.testing
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

14) import derivepassphrase as dpp
15) import derivepassphrase.cli as cli
16) import ssh_agent_client.types
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

17) import pytest
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

18) import tests
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

19) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

20) DUMMY_SERVICE = tests.DUMMY_SERVICE
21) DUMMY_PASSPHRASE = tests.DUMMY_PASSPHRASE
22) DUMMY_CONFIG_SETTINGS = tests.DUMMY_CONFIG_SETTINGS
23) DUMMY_RESULT_PASSPHRASE = tests.DUMMY_RESULT_PASSPHRASE
24) DUMMY_RESULT_KEY1 = tests.DUMMY_RESULT_KEY1
25) DUMMY_PHRASE_FROM_KEY1_RAW = tests.DUMMY_PHRASE_FROM_KEY1_RAW
26) DUMMY_PHRASE_FROM_KEY1 = tests.DUMMY_PHRASE_FROM_KEY1
27) 
28) DUMMY_KEY1 = tests.DUMMY_KEY1
29) DUMMY_KEY1_B64 = tests.DUMMY_KEY1_B64
30) DUMMY_KEY2 = tests.DUMMY_KEY2
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

31) 
32) 
33) class IncompatibleConfiguration(NamedTuple):
34)     other_options: list[tuple[str, ...]]
35)     needs_service: bool | None
36)     input: bytes | None
37) 
38) class SingleConfiguration(NamedTuple):
39)     needs_service: bool | None
40)     input: bytes | None
41)     check_success: bool
42) 
43) class OptionCombination(NamedTuple):
44)     options: list[str]
45)     incompatible: bool
46)     needs_service: bool | None
47)     input: bytes | None
48)     check_success: bool
49) 
50) PASSWORD_GENERATION_OPTIONS: list[tuple[str, ...]] = [
51)     ('--phrase',), ('--key',), ('--length', '20'), ('--repeat', '20'),
52)     ('--lower', '1'), ('--upper', '1'), ('--number', '1'),
53)     ('--space', '1'), ('--dash', '1'), ('--symbol', '1')
54) ]
55) CONFIGURATION_OPTIONS: list[tuple[str, ...]] = [
56)     ('--notes',), ('--config',), ('--delete',), ('--delete-globals',),
57)     ('--clear',)
58) ]
59) CONFIGURATION_COMMANDS: list[tuple[str, ...]] = [
60)     ('--notes',), ('--delete',), ('--delete-globals',), ('--clear',)
61) ]
62) STORAGE_OPTIONS: list[tuple[str, ...]] = [
63)     ('--export', '-'), ('--import', '-')
64) ]
65) INCOMPATIBLE: dict[tuple[str, ...], IncompatibleConfiguration] = {
66)     ('--phrase',): IncompatibleConfiguration(
67)         [('--key',)] + CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
68)         True, DUMMY_PASSPHRASE),
69)     ('--key',): IncompatibleConfiguration(
70)         CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
71)         True, DUMMY_PASSPHRASE),
72)     ('--length', '20'): IncompatibleConfiguration(
73)         CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
74)         True, DUMMY_PASSPHRASE),
75)     ('--repeat', '20'): IncompatibleConfiguration(
76)         CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
77)         True, DUMMY_PASSPHRASE),
78)     ('--lower', '1'): IncompatibleConfiguration(
79)         CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
80)         True, DUMMY_PASSPHRASE),
81)     ('--upper', '1'): IncompatibleConfiguration(
82)         CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
83)         True, DUMMY_PASSPHRASE),
84)     ('--number', '1'): IncompatibleConfiguration(
85)         CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
86)         True, DUMMY_PASSPHRASE),
87)     ('--space', '1'): IncompatibleConfiguration(
88)         CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
89)         True, DUMMY_PASSPHRASE),
90)     ('--dash', '1'): IncompatibleConfiguration(
91)         CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
92)         True, DUMMY_PASSPHRASE),
93)     ('--symbol', '1'): IncompatibleConfiguration(
94)         CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
95)         True, DUMMY_PASSPHRASE),
96)     ('--notes',): IncompatibleConfiguration(
97)         [('--config',), ('--delete',), ('--delete-globals',),
98)          ('--clear',)] + STORAGE_OPTIONS,
99)         True, None),
100)     ('--config', '-p'): IncompatibleConfiguration(
101)         [('--delete',), ('--delete-globals',),
102)          ('--clear',)] + STORAGE_OPTIONS,
103)         None, DUMMY_PASSPHRASE),
104)     ('--delete',): IncompatibleConfiguration(
105)         [('--delete-globals',), ('--clear',)] + STORAGE_OPTIONS, True, None),
106)     ('--delete-globals',): IncompatibleConfiguration(
107)         [('--clear',)] + STORAGE_OPTIONS, False, None),
108)     ('--clear',): IncompatibleConfiguration(STORAGE_OPTIONS, False, None),
109)     ('--export', '-'): IncompatibleConfiguration(
110)         [('--import', '-')], False, None),
111)     ('--import', '-'): IncompatibleConfiguration(
112)         [], False, None),
113) }
114) SINGLES: dict[tuple[str, ...], SingleConfiguration] = {
115)     ('--phrase',): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
116)     ('--key',): SingleConfiguration(True, None, False),
117)     ('--length', '20'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
118)     ('--repeat', '20'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
119)     ('--lower', '1'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
120)     ('--upper', '1'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
121)     ('--number', '1'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
122)     ('--space', '1'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
123)     ('--dash', '1'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
124)     ('--symbol', '1'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
125)     ('--notes',): SingleConfiguration(True, None, False),
126)     ('--config', '-p'): SingleConfiguration(None, DUMMY_PASSPHRASE, False),
127)     ('--delete',): SingleConfiguration(True, None, False),
128)     ('--delete-globals',): SingleConfiguration(False, None, True),
129)     ('--clear',): SingleConfiguration(False, None, True),
130)     ('--export', '-'): SingleConfiguration(False, None, True),
131)     ('--import', '-'): SingleConfiguration(False, b'{"services": {}}', True),
132) }
133) INTERESTING_OPTION_COMBINATIONS: list[OptionCombination] = []
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

134) config: IncompatibleConfiguration | SingleConfiguration
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

135) for opt, config in INCOMPATIBLE.items():
136)     for opt2 in config.other_options:
137)         INTERESTING_OPTION_COMBINATIONS.extend([
138)             OptionCombination(options=list(opt + opt2), incompatible=True,
139)                               needs_service=config.needs_service,
140)                               input=config.input, check_success=False),
141)             OptionCombination(options=list(opt2 + opt), incompatible=True,
142)                               needs_service=config.needs_service,
143)                               input=config.input, check_success=False)
144)         ])
145) for opt, config in SINGLES.items():
146)     INTERESTING_OPTION_COMBINATIONS.append(
147)         OptionCombination(options=list(opt), incompatible=False,
148)                           needs_service=config.needs_service,
149)                           input=config.input,
150)                           check_success=config.check_success))
151) 
152) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

153) class TestCLI:
154)     def test_200_help_output(self):
155)         runner = click.testing.CliRunner(mix_stderr=False)
156)         result = runner.invoke(cli.derivepassphrase, ['--help'],
157)                                catch_exceptions=False)
158)         assert result.exit_code == 0
159)         assert 'Password generation:\n' in result.output, (
160)             'Option groups not respected in help text.'
161)         )
162)         assert 'Use NUMBER=0, e.g. "--symbol 0"' in result.output, (
163)             'Option group epilog not printed.'
164)         )
165) 
166)     @pytest.mark.parametrize(['charset_name'],
167)                              [('lower',), ('upper',), ('number',), ('space',),
168)                               ('dash',), ('symbol',)])
169)     def test_201_disable_character_set(
170)         self, monkeypatch: Any, charset_name: str
171)     ) -> None:
172)         monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt)
173)         option = f'--{charset_name}'
174)         charset = dpp.Vault._CHARSETS[charset_name].decode('ascii')
175)         runner = click.testing.CliRunner(mix_stderr=False)
176)         result = runner.invoke(cli.derivepassphrase,
177)                                [option, '0', '-p', DUMMY_SERVICE],
178)                                input=DUMMY_PASSPHRASE, catch_exceptions=False)
179)         assert result.exit_code == 0, (
180)             f'program died unexpectedly with exit code {result.exit_code}'
181)         )
182)         assert not result.stderr_bytes, (
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

183)             f'program barfed on stderr: {result.stderr_bytes!r}'
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

184)         )
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

185)         for c in charset:
186)             assert c not in result.stdout, (
187)                 f'derived password contains forbidden character {c!r}: '
188)                 f'{result.stdout!r}'
189)             )
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

190) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

191)     def test_202_disable_repetition(self, monkeypatch: Any) -> None:
192)         monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt)
193)         runner = click.testing.CliRunner(mix_stderr=False)
194)         result = runner.invoke(cli.derivepassphrase,
195)                                ['--repeat', '0', '-p', DUMMY_SERVICE],
196)                                input=DUMMY_PASSPHRASE, catch_exceptions=False)
197)         assert result.exit_code == 0, (
198)             f'program died unexpectedly with exit code {result.exit_code}'
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

199)         )
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

200)         assert not result.stderr_bytes, (
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

201)             f'program barfed on stderr: {result.stderr_bytes!r}'
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

202)         )
203)         passphrase = result.stdout.rstrip('\r\n')
204)         for i in range(len(passphrase) - 1):
205)             assert passphrase[i:i+1] != passphrase[i+1:i+2], (
206)                 f'derived password contains repeated character '
207)                 f'at position {i}: {result.stdout!r}'
208)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

209) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

210)     @pytest.mark.parametrize(['config'], [
211)         pytest.param({'global': {'key': DUMMY_KEY1_B64},
212)                       'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
213)                      id='global'),
214)         pytest.param({'global': {'phrase': DUMMY_PASSPHRASE.rstrip(b'\n')
215)                                            .decode('ASCII')},
216)                       'services': {DUMMY_SERVICE: {'key': DUMMY_KEY1_B64,
217)                                                    **DUMMY_CONFIG_SETTINGS}}},
218)                      id='service'),
219)     ])
220)     def test_204a_key_from_config(
221)         self, monkeypatch: Any, config: dpp.types.VaultConfig,
222)     ) -> None:
223)         runner = click.testing.CliRunner(mix_stderr=False)
224)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
225)                                    config=config):
226)             monkeypatch.setattr(dpp.Vault, 'phrase_from_key',
227)                                 tests.phrase_from_key)
228)             result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE],
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

229)                                    catch_exceptions=False)
230)             assert (result.exit_code, result.stderr_bytes) == (0, b''), (
231)                 'program exited with failure'
232)             )
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

233)             assert (
234)                 result.stdout_bytes.rstrip(b'\n') != DUMMY_RESULT_PASSPHRASE
235)             ), (
236)                 'program generated unexpected result (phrase instead of key)'
237)             )
238)             assert result.stdout_bytes.rstrip(b'\n') == DUMMY_RESULT_KEY1, (
239)                 'program generated unexpected result (wrong settings?)'
240)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

241) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

242)     def test_204b_key_from_command_line(self, monkeypatch: Any) -> None:
243)         runner = click.testing.CliRunner(mix_stderr=False)
244)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
245)                                    config={'services': {DUMMY_SERVICE:
246)                                                DUMMY_CONFIG_SETTINGS}}):
247)             monkeypatch.setattr(cli, '_get_suitable_ssh_keys',
248)                                 tests.suitable_ssh_keys)
249)             monkeypatch.setattr(dpp.Vault, 'phrase_from_key',
250)                                 tests.phrase_from_key)
251)             result = runner.invoke(cli.derivepassphrase,
252)                                    ['-k', DUMMY_SERVICE],
253)                                    input=b'1\n', catch_exceptions=False)
254)             assert result.exit_code == 0, 'program exited with failure'
255)             assert result.stdout_bytes, 'program output expected'
256)             last_line = result.stdout_bytes.splitlines(True)[-1]
257)             assert last_line.rstrip(b'\n') != DUMMY_RESULT_PASSPHRASE, (
258)                 'program generated unexpected result (phrase instead of key)'
259)             )
260)             assert last_line.rstrip(b'\n') == DUMMY_RESULT_KEY1, (
261)                 'program generated unexpected result (wrong settings?)'
262)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

263) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

264)     def test_205_service_phrase_if_key_in_global_config(
265)         self, monkeypatch: Any,
266)     ) -> None:
267)         runner = click.testing.CliRunner(mix_stderr=False)
268)         with tests.isolated_config(
269)             monkeypatch=monkeypatch, runner=runner,
270)             config={
271)                 'global': {'key': DUMMY_KEY1_B64},
272)                 'services': {
273)                     DUMMY_SERVICE: {
274)                         'phrase': DUMMY_PASSPHRASE.rstrip(b'\n')
275)                                   .decode('ASCII'),
276)                         **DUMMY_CONFIG_SETTINGS}}}
277)         ):
278)             result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE],
279)                                    catch_exceptions=False)
280)             assert result.exit_code == 0, 'program exited with failure'
281)             assert result.stdout_bytes, 'program output expected'
282)             last_line = result.stdout_bytes.splitlines(True)[-1]
283)             assert last_line.rstrip(b'\n') != DUMMY_RESULT_KEY1, (
284)                 'program generated unexpected result (key instead of phrase)'
285)             )
286)             assert last_line.rstrip(b'\n') == DUMMY_RESULT_PASSPHRASE, (
287)                 'program generated unexpected result (wrong settings?)'
288)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

289) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

290)     @pytest.mark.parametrize(['option'],
291)                              [('--lower',), ('--upper',), ('--number',),
292)                               ('--space',), ('--dash',), ('--symbol',),
293)                               ('--repeat',), ('--length',)])
294)     def test_210_invalid_argument_range(self, option: str) -> None:
295)         runner = click.testing.CliRunner(mix_stderr=False)
296)         value: str | int
297)         for value in '-42', 'invalid':
298)             result = runner.invoke(cli.derivepassphrase,
299)                                    [option, cast(str, value), '-p',
300)                                     DUMMY_SERVICE],
301)                                    input=DUMMY_PASSPHRASE,
302)                                    catch_exceptions=False)
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

303)             assert result.exit_code > 0, (
304)                 'program unexpectedly succeeded'
305)             )
306)             assert result.stderr_bytes, (
307)                 'program did not print any error message'
308)             )
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

309)             assert b'Error: Invalid value' in result.stderr_bytes, (
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

310)                 'program did not print the expected error message'
311)             )
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

312) 
313)     @pytest.mark.parametrize(
314)         ['options', 'service', 'input', 'check_success'],
315)         [(o.options, o.needs_service, o.input, o.check_success)
316)          for o in INTERESTING_OPTION_COMBINATIONS if not o.incompatible],
317)     )
318)     def test_211_service_needed(
319)         self, monkeypatch: Any, options: list[str],
320)         service: bool | None, input: bytes | None, check_success: bool,
321)     ) -> None:
322)         monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt)
323)         runner = click.testing.CliRunner(mix_stderr=False)
324)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
325)                                    config={'global': {'phrase': 'abc'},
326)                                            'services': {}}):
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

327)             result = runner.invoke(cli.derivepassphrase,
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

328)                                    options if service
329)                                    else options + [DUMMY_SERVICE],
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

330)                                    input=input, catch_exceptions=False)
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

331)             if service is not None:
332)                 assert result.exit_code > 0, (
333)                     'program unexpectedly succeeded'
334)                 )
335)                 assert result.stderr_bytes, (
336)                     'program did not print any error message'
337)                 )
338)                 err_msg = (b' requires a SERVICE' if service
339)                            else b' does not take a SERVICE argument')
340)                 assert err_msg in result.stderr_bytes, (
341)                     'program did not print the expected error message'
342)                 )
343)             else:
344)                 assert (result.exit_code, result.stderr_bytes) == (0, b''), (
345)                     'program unexpectedly failed'
346)                 )
347)         if check_success:
348)             with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
349)                                        config={'global': {'phrase': 'abc'},
350)                                                'services': {}}):
351)                 monkeypatch.setattr(cli, '_prompt_for_passphrase',
352)                                     tests.auto_prompt)
353)                 result = runner.invoke(cli.derivepassphrase,
354)                                        options + [DUMMY_SERVICE]
355)                                        if service else options,
356)                                        input=input, catch_exceptions=False)
357)                 assert (result.exit_code, result.stderr_bytes) == (0, b''), (
358)                     'program unexpectedly failed'
359)                 )
360) 
361)     @pytest.mark.parametrize(
362)         ['options', 'service', 'input'],
363)         [(o.options, o.needs_service, o.input)
364)          for o in INTERESTING_OPTION_COMBINATIONS if o.incompatible],
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

365)     )
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

366)     def test_212_incompatible_options(
367)         self, options: list[str], service: bool | None, input: bytes | None,
368)     ) -> None:
369)         runner = click.testing.CliRunner(mix_stderr=False)
370)         result = runner.invoke(cli.derivepassphrase,
371)                                options + [DUMMY_SERVICE] if service
372)                                else options,
373)                                input=DUMMY_PASSPHRASE, catch_exceptions=False)
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

374)         assert result.exit_code > 0, (
375)             'program unexpectedly succeeded'
376)         )
377)         assert result.stderr_bytes, (
378)             'program did not print any error message'
379)         )
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

380)         assert b'mutually exclusive with ' in result.stderr_bytes, (
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

381)             'program did not print the expected error message'
382)         )
383) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

384)     def test_213_import_bad_config_not_vault_config(
385)         self, monkeypatch: Any,
386)     ) -> None:
387)         runner = click.testing.CliRunner(mix_stderr=False)
388)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
389)                                    config={'services': {}}):
390)             result = runner.invoke(cli.derivepassphrase, ['--import', '-'],
391)                                    input=b'null', catch_exceptions=False)
392)             assert result.exit_code > 0, (
393)                 'program unexpectedly succeeded'
394)             )
395)             assert result.stderr_bytes, (
396)                 'program did not print any error message'
397)             )
398)             assert b'not a valid config' in result.stderr_bytes, (
399)                 'program did not print the expected error message'
400)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

401) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

402)     def test_213a_import_bad_config_not_json_data(
403)         self, monkeypatch: Any,
404)     ) -> None:
405)         runner = click.testing.CliRunner(mix_stderr=False)
406)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
407)                                    config={'services': {}}):
408)             result = runner.invoke(cli.derivepassphrase, ['--import', '-'],
409)                                    input=b'This string is not valid JSON.',
410)                                    catch_exceptions=False)
411)             assert result.exit_code > 0, (
412)                 'program unexpectedly succeeded'
413)             )
414)             assert result.stderr_bytes, (
415)                 'program did not print any error message'
416)             )
417)             assert b'cannot decode JSON' in result.stderr_bytes, (
418)                 'program did not print the expected error message'
419)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

420) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

421)     def test_213b_import_bad_config_not_a_file(
422)         self, monkeypatch: Any,
423)     ) -> None:
424)         runner = click.testing.CliRunner(mix_stderr=False)
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

425)         # `isolated_config` validates the configuration.  So, to pass an
426)         # actual broken configuration, we must open the configuration file
427)         # ourselves afterwards, inside the context.
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

428)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
429)                                    config={'services': {}}):
430)             with open(cli._config_filename(), 'wt') as outfile:
431)                 print('This string is not valid JSON.', file=outfile)
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

432)             dname = os.path.dirname(cli._config_filename())
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

433)             result = runner.invoke(
434)                 cli.derivepassphrase,
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

435)                 ['--import', os.fsdecode(dname)],
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

436)                 catch_exceptions=False)
437)             assert result.exit_code > 0, (
438)                 'program unexpectedly succeeded'
439)             )
440)             assert result.stderr_bytes, (
441)                 'program did not print any error message'
442)             )
443)             # Don't test the actual error message, because it is subject to
444)             # locale settings.  TODO: find a way anyway.
445) 
446)     def test_214_export_settings_no_stored_settings(
447)         self, monkeypatch: Any,
448)     ) -> None:
449)         runner = click.testing.CliRunner(mix_stderr=False)
450)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
451)                                    config={'services': {}}):
452)             try:
453)                 os.remove(cli._config_filename())
454)             except FileNotFoundError:  # pragma: no cover
455)                 pass
456)             result = runner.invoke(cli.derivepassphrase, ['--export', '-'],
457)                                    catch_exceptions=False)
458)             assert (result.exit_code, result.stderr_bytes) == (0, b''), (
459)                 'program exited with failure'
460)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

461) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

462)     def test_214a_export_settings_bad_stored_config(
463)         self, monkeypatch: Any,
464)     ) -> None:
465)         runner = click.testing.CliRunner(mix_stderr=False)
466)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
467)                                    config={}):
468)             result = runner.invoke(cli.derivepassphrase, ['--export', '-'],
469)                                    input=b'null', catch_exceptions=False)
470)             assert result.exit_code > 0, (
471)                 'program unexpectedly succeeded'
472)             )
473)             assert result.stderr_bytes, (
474)                 'program did not print any error message'
475)             )
476)             assert b'cannot load config' in result.stderr_bytes, (
477)                 'program did not print the expected error message'
478)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

479) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

480)     def test_214b_export_settings_not_a_file(
481)         self, monkeypatch: Any,
482)     ) -> None:
483)         runner = click.testing.CliRunner(mix_stderr=False)
484)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
485)                                    config={'services': {}}):
486)             try:
487)                 os.remove(cli._config_filename())
488)             except FileNotFoundError:  # pragma: no cover
489)                 pass
490)             os.makedirs(cli._config_filename())
491)             result = runner.invoke(cli.derivepassphrase, ['--export', '-'],
492)                                    input=b'null', catch_exceptions=False)
493)             assert result.exit_code > 0, (
494)                 'program unexpectedly succeeded'
495)             )
496)             assert result.stderr_bytes, (
497)                 'program did not print any error message'
498)             )
499)             assert b'cannot load config' in result.stderr_bytes, (
500)                 'program did not print the expected error message'
501)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

502) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

503)     def test_214c_export_settings_target_not_a_file(
504)         self, monkeypatch: Any,
505)     ) -> None:
506)         runner = click.testing.CliRunner(mix_stderr=False)
507)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
508)                                    config={'services': {}}):
509)             dname = os.path.dirname(cli._config_filename())
510)             result = runner.invoke(cli.derivepassphrase,
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

511)                                    ['--export', os.fsdecode(dname)],
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

512)                                    input=b'null', catch_exceptions=False)
513)             assert result.exit_code > 0, (
514)                 'program unexpectedly succeeded'
515)             )
516)             assert result.stderr_bytes, (
517)                 'program did not print any error message'
518)             )
519)             assert b'cannot write config' in result.stderr_bytes, (
520)                 'program did not print the expected error message'
521)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

522) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

523)     def test_220_edit_notes_successfully(self, monkeypatch: Any) -> None:
524)         edit_result = '''
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

525) 
526) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
527) contents go here
528) '''
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

529)         runner = click.testing.CliRunner(mix_stderr=False)
530)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
531)                                    config={'global': {'phrase': 'abc'},
532)                                            'services': {}}):
533)             monkeypatch.setattr(click, 'edit',
534)                                 lambda *a, **kw: edit_result)
535)             result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'],
536)                                    catch_exceptions=False)
537)             assert (result.exit_code, result.stderr_bytes) == (0, b''), (
538)                 'program exited with failure'
539)             )
540)             with open(cli._config_filename(), 'rt') as infile:
541)                 config = json.load(infile)
542)             assert config == {'global': {'phrase': 'abc'},
543)                               'services': {'sv': {'notes':
544)                                                       'contents go here'}}}
545) 
546)     def test_221_edit_notes_noop(self, monkeypatch: Any) -> None:
547)         runner = click.testing.CliRunner(mix_stderr=False)
548)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
549)                                    config={'global': {'phrase': 'abc'},
550)                                            'services': {}}):
551)             monkeypatch.setattr(click, 'edit', lambda *a, **kw: None)
552)             result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'],
553)                                    catch_exceptions=False)
554)             assert (result.exit_code, result.stderr_bytes) == (0, b''), (
555)                 'program exited with failure'
556)             )
557)             with open(cli._config_filename(), 'rt') as infile:
558)                 config = json.load(infile)
559)             assert config == {'global': {'phrase': 'abc'}, 'services': {}}
560) 
561)     def test_222_edit_notes_marker_removed(self, monkeypatch: Any) -> None:
562)         runner = click.testing.CliRunner(mix_stderr=False)
563)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
564)                                    config={'global': {'phrase': 'abc'},
565)                                            'services': {}}):
566)             monkeypatch.setattr(click, 'edit', lambda *a, **kw: 'long\ntext')
567)             result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'],
568)                                    catch_exceptions=False)
569)             assert (result.exit_code, result.stderr_bytes) == (0, b''), (
570)                 'program exited with failure'
571)             )
572)             with open(cli._config_filename(), 'rt') as infile:
573)                 config = json.load(infile)
574)             assert config == {'global': {'phrase': 'abc'},
575)                               'services': {'sv': {'notes': 'long\ntext'}}}
576) 
577)     def test_223_edit_notes_abort(self, monkeypatch: Any) -> None:
578)         runner = click.testing.CliRunner(mix_stderr=False)
579)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
580)                                    config={'global': {'phrase': 'abc'},
581)                                            'services': {}}):
582)             monkeypatch.setattr(click, 'edit', lambda *a, **kw: '\n\n')
583)             result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'],
584)                                    catch_exceptions=False)
585)             assert result.exit_code != 0, 'program unexpectedly succeeded'
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

586)             assert result.stderr_bytes is not None
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

587)             assert b'user aborted request' in result.stderr_bytes, (
588)                 'expected error message missing'
589)             )
590)             with open(cli._config_filename(), 'rt') as infile:
591)                 config = json.load(infile)
592)             assert config == {'global': {'phrase': 'abc'}, 'services': {}}
593) 
594)     @pytest.mark.parametrize(['command_line', 'input', 'result_config'], [
595)         (
596)             ['--phrase'],
597)             b'my passphrase\n',
598)             {'global': {'phrase': 'my passphrase'}, 'services': {}},
599)         ),
600)         (
601)             ['--key'],
602)             b'1\n',
603)             {'global': {'key': DUMMY_KEY1_B64}, 'services': {}},
604)         ),
605)         (
606)             ['--phrase', 'sv'],
607)             b'my passphrase\n',
608)             {'global': {'phrase': 'abc'},
609)              'services': {'sv': {'phrase': 'my passphrase'}}},
610)         ),
611)         (
612)             ['--key', 'sv'],
613)             b'1\n',
614)             {'global': {'phrase': 'abc'},
615)              'services': {'sv': {'key': DUMMY_KEY1_B64}}},
616)         ),
617)         (
618)             ['--key', '--length', '15', 'sv'],
619)             b'1\n',
620)             {'global': {'phrase': 'abc'},
621)              'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}}},
622)         ),
623)     ])
624)     def test_224_store_config_good(
625)         self, monkeypatch: Any, command_line: list[str], input: bytes,
626)         result_config: Any,
627)     ) -> None:
628)         runner = click.testing.CliRunner(mix_stderr=False)
629)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
630)                                    config={'global': {'phrase': 'abc'},
631)                                            'services': {}}):
632)             monkeypatch.setattr(cli, '_get_suitable_ssh_keys',
633)                                 tests.suitable_ssh_keys)
634)             result = runner.invoke(cli.derivepassphrase,
635)                                    ['--config'] + command_line,
636)                                    catch_exceptions=False, input=input)
637)             assert result.exit_code == 0, 'program exited with failure'
638)             with open(cli._config_filename(), 'rt') as infile:
639)                 config = json.load(infile)
640)             assert config == result_config, (
641)                 'stored config does not match expectation'
642)             )
643) 
644)     @pytest.mark.parametrize(['command_line', 'input', 'err_text'], [
645)         ([], b'', b'cannot update global settings without actual settings'),
646)         (
647)             ['sv'],
648)             b'',
649)             b'cannot update service settings without actual settings',
650)         ),
651)         (['--phrase', 'sv'], b'', b'no passphrase given'),
652)         (['--key'], b'', b'no valid SSH key selected'),
653)     ])
654)     def test_225_store_config_fail(
655)         self, monkeypatch: Any, command_line: list[str],
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

656)         input: bytes, err_text: bytes,
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

657)     ) -> None:
658)         runner = click.testing.CliRunner(mix_stderr=False)
659)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
660)                                    config={'global': {'phrase': 'abc'},
661)                                            'services': {}}):
662)             monkeypatch.setattr(cli, '_get_suitable_ssh_keys',
663)                                 tests.suitable_ssh_keys)
664)             result = runner.invoke(cli.derivepassphrase,
665)                                    ['--config'] + command_line,
666)                                    catch_exceptions=False, input=input)
667)             assert result.exit_code != 0, 'program unexpectedly succeeded?!'
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

668)             assert result.stderr_bytes is not None
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

669)             assert err_text in result.stderr_bytes, (
670)                 'expected error message missing'
671)             )
672) 
673)     def test_225a_store_config_fail_manual_no_ssh_key_selection(
674)         self, monkeypatch: Any,
675)     ) -> None:
676)         runner = click.testing.CliRunner(mix_stderr=False)
677)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
678)                                    config={'global': {'phrase': 'abc'},
679)                                            'services': {}}):
680)             def raiser():
681)                 raise RuntimeError('custom error message')
682)             monkeypatch.setattr(cli, '_select_ssh_key', raiser)
683)             result = runner.invoke(cli.derivepassphrase,
684)                                    ['--key', '--config'],
685)                                    catch_exceptions=False)
686)             assert result.exit_code != 0, 'program unexpectedly succeeded'
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

687)             assert result.stderr_bytes is not None
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

688)             assert b'custom error message' in result.stderr_bytes, (
689)                 'expected error message missing'
690)             )
691) 
692)     def test_226_no_arguments(self) -> None:
693)         runner = click.testing.CliRunner(mix_stderr=False)
694)         result = runner.invoke(cli.derivepassphrase, [],
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

695)                                catch_exceptions=False)
696)         assert result.exit_code != 0, 'program unexpectedly succeeded'
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

697)         assert result.stderr_bytes is not None
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

698)         assert b'SERVICE is required' in result.stderr_bytes, (
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

699)             'expected error message missing'
700)         )
701) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

702)     def test_226a_no_passphrase_or_key(self) -> None:
703)         runner = click.testing.CliRunner(mix_stderr=False)
704)         result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE],
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

705)                                catch_exceptions=False)
706)         assert result.exit_code != 0, 'program unexpectedly succeeded'
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

707)         assert result.stderr_bytes is not None
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

708)         assert b'no passphrase or key given' in result.stderr_bytes, (
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

709)             'expected error message missing'
710)         )
711) 
712) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

713) class TestCLIUtils:
714) 
715)     def test_100_save_bad_config(self, monkeypatch: Any) -> None:
716)         runner = click.testing.CliRunner()
717)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
718)                                    config={}):
719)             with pytest.raises(ValueError, match='Invalid vault config'):
720)                 cli._save_config(None)  # type: ignore
721) 
722) 
723)     def test_101_prompt_for_selection_multiple(self, monkeypatch: Any) -> None:
724)         @click.command()
725)         @click.option('--heading', default='Our menu:')
726)         @click.argument('items', nargs=-1)
727)         def driver(heading, items):
728)             # from https://montypython.fandom.com/wiki/Spam#The_menu
729)             items = items or [
730)                 'Egg and bacon',
731)                 'Egg, sausage and bacon',
732)                 'Egg and spam',
733)                 'Egg, bacon and spam',
734)                 'Egg, bacon, sausage and spam',
735)                 'Spam, bacon, sausage and spam',
736)                 'Spam, egg, spam, spam, bacon and spam',
737)                 'Spam, spam, spam, egg and spam',
738)                 ('Spam, spam, spam, spam, spam, spam, baked beans, '
739)                  'spam, spam, spam and spam'),
740)                 ('Lobster thermidor aux crevettes with a mornay sauce '
741)                  'garnished with truffle paté, brandy '
742)                  'and a fried egg on top and spam'),
743)             ]
744)             index = cli._prompt_for_selection(items, heading=heading)
745)             click.echo('A fine choice: ', nl=False)
746)             click.echo(items[index])
747)             click.echo('(Note: Vikings strictly optional.)')
748)         runner = click.testing.CliRunner(mix_stderr=True)
749)         result = runner.invoke(driver, [], input='9')
750)         assert result.exit_code == 0, 'driver program failed'
751)         assert result.stdout == '''\
752) Our menu:
753) [1] Egg and bacon
754) [2] Egg, sausage and bacon
755) [3] Egg and spam
756) [4] Egg, bacon and spam
757) [5] Egg, bacon, sausage and spam
758) [6] Spam, bacon, sausage and spam
759) [7] Spam, egg, spam, spam, bacon and spam
760) [8] Spam, spam, spam, egg and spam
761) [9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
762) [10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam
763) Your selection? (1-10, leave empty to abort): 9
764) A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
765) (Note: Vikings strictly optional.)
766) ''', 'driver program produced unexpected output'
767)         result = runner.invoke(driver, ['--heading='], input='',
768)                                catch_exceptions=True)
769)         assert result.exit_code > 0, 'driver program succeeded?!'
770)         assert result.stdout == '''\
771) [1] Egg and bacon
772) [2] Egg, sausage and bacon
773) [3] Egg and spam
774) [4] Egg, bacon and spam
775) [5] Egg, bacon, sausage and spam
776) [6] Spam, bacon, sausage and spam
777) [7] Spam, egg, spam, spam, bacon and spam
778) [8] Spam, spam, spam, egg and spam
779) [9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
780) [10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam
781) Your selection? (1-10, leave empty to abort): \n''', (
782)             'driver program produced unexpected output'
783)         )
784)         assert isinstance(result.exception, IndexError), (
785)             'driver program did not raise IndexError?!'
786)         )
787) 
788) 
789)     def test_102_prompt_for_selection_single(self, monkeypatch: Any) -> None:
790)         @click.command()
791)         @click.option('--item', default='baked beans')
792)         @click.argument('prompt')
793)         def driver(item, prompt):
794)             try:
795)                 cli._prompt_for_selection([item], heading='',
796)                                           single_choice_prompt=prompt)
797)             except IndexError as e:
798)                 click.echo('Boo.')
799)                 raise e
800)             else:
801)                 click.echo('Great!')
802)         runner = click.testing.CliRunner(mix_stderr=True)
803)         result = runner.invoke(driver, ['Will replace with spam. Confirm, y/n?'],
804)                                input='y')
805)         assert result.exit_code == 0, 'driver program failed'
806)         assert result.stdout == '''\
807) [1] baked beans
808) Will replace with spam. Confirm, y/n? y
809) Great!
810) ''', 'driver program produced unexpected output'
811)         result = runner.invoke(driver,
812)                                ['Will replace with spam, okay? ' +
813)                                 '(Please say "y" or "n".)'],
814)                                input='')
815)         assert result.exit_code > 0, 'driver program succeeded?!'
816)         assert result.stdout == '''\
817) [1] baked beans
818) Will replace with spam, okay? (Please say "y" or "n".): 
819) Boo.
820) ''', 'driver program produced unexpected output'
821)         assert isinstance(result.exception, IndexError), (
822)             'driver program did not raise IndexError?!'
823)         )
824) 
825) 
826)     def test_103_prompt_for_passphrase(self, monkeypatch: Any) -> None:
827)         monkeypatch.setattr(click, 'prompt',
828)                             lambda *a, **kw: json.dumps({'args': a, 'kwargs': kw}))
829)         res = json.loads(cli._prompt_for_passphrase())
830)         assert 'args' in res and 'kwargs' in res, (
831)             'missing arguments to passphrase prompt'
832)         )
833)         assert res['args'][:1] == ['Passphrase'], (
834)             'missing arguments to passphrase prompt'
835)         )
836)         assert (res['kwargs'].get('default') == ''
837)                 and not res['kwargs'].get('show_default', True)), (
838)             'missing arguments to passphrase prompt'
839)         )
840)         assert res['kwargs'].get('err') and res['kwargs'].get('hide_input'), (
841)             'missing arguments to passphrase prompt'
842)         )
843) 
844) 
845)     @pytest.mark.parametrize(['command_line', 'config', 'result_config'], [
846)         (['--delete-globals'],
847)          {'global': {'phrase': 'abc'}, 'services': {}}, {'services': {}}),
848)         (['--delete', DUMMY_SERVICE],
849)          {'global': {'phrase': 'abc'},
850)           'services': {DUMMY_SERVICE: {'notes': '...'}}},
851)          {'global': {'phrase': 'abc'}, 'services': {}}),
852)         (['--clear'],
853)          {'global': {'phrase': 'abc'},
854)           'services': {DUMMY_SERVICE: {'notes': '...'}}},
855)          {'services': {}}),
856)     ])
857)     def test_203_repeated_config_deletion(
858)         self, monkeypatch: Any, command_line: list[str],
859)         config: dpp.types.VaultConfig, result_config: dpp.types.VaultConfig,
860)     ) -> None:
861)         runner = click.testing.CliRunner(mix_stderr=False)
862)         for start_config in [config, result_config]:
863)             with tests.isolated_config(monkeypatch=monkeypatch,
864)                                        runner=runner, config=start_config):
865)                 result = runner.invoke(cli.derivepassphrase, command_line,
866)                                        catch_exceptions=False)
867)                 assert (result.exit_code, result.stderr_bytes) == (0, b''), (
868)                     'program exited with failure'
869)                 )
870)                 with open(cli._config_filename(), 'rt') as infile:
871)                     config_readback = json.load(infile)
872)                 assert config_readback == result_config
873) 
874) 
875)     def test_204_phrase_from_key_manually(self) -> None:
876)         assert (
877)             dpp.Vault(phrase=DUMMY_PHRASE_FROM_KEY1, **DUMMY_CONFIG_SETTINGS)
878)             .generate(DUMMY_SERVICE) == DUMMY_RESULT_KEY1
879)         )
880) 
881) 
882)     @pytest.mark.parametrize(['vfunc', 'input'], [
883)         (cli._validate_occurrence_constraint, 20),
884)         (cli._validate_length, 20),
885)     ])
886)     def test_210a_validate_constraints_manually(
887)         self,
888)         vfunc: Callable[[click.Context, click.Parameter, Any], int | None],
889)         input: int,
890)     ) -> None:
891)         ctx = cli.derivepassphrase.make_context(cli.prog_name, [])
892)         param = cli.derivepassphrase.params[0]
893)         assert vfunc(ctx, param, input) == input
894) 
895) 
896)     @tests.skip_if_no_agent
897)     @pytest.mark.parametrize(['conn_hint'],
898)                              [('none',), ('socket',), ('client',)])
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

899)     def test_227_get_suitable_ssh_keys(
900)         self, monkeypatch: Any, conn_hint: str,
901)     ) -> None: