98fbb55ed0e63caa328ca4ef587578c0b0566a45
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) 
7) import base64
Marco Ricci Fix miscellaneous type chec...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

11) import socket
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

12) from typing import Any, cast, TYPE_CHECKING, NamedTuple
13) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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