bb75e2c43f4f1e88d0c717761e98457b774fb6e8
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
8) import json
9) import os
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

11) from typing import Any, cast, TYPE_CHECKING, NamedTuple
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] = []
134) config: OptionCombination | SingleConfiguration
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, (
183)             f'program barfed on stderr: {result.stderr_bytes}'
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, (
201)             f'program barfed on stderr: {result.stderr_bytes}'
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)
425)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
426)                                    config={'services': {}}):
427)             with open(cli._config_filename(), 'wt') as outfile:
428)                 print('This string is not valid JSON.', file=outfile)
429)             result = runner.invoke(
430)                 cli.derivepassphrase,
431)                 ['--import', os.path.dirname(cli._config_filename())],
432)                 catch_exceptions=False)
433)             assert result.exit_code > 0, (
434)                 'program unexpectedly succeeded'
435)             )
436)             assert result.stderr_bytes, (
437)                 'program did not print any error message'
438)             )
439)             # Don't test the actual error message, because it is subject to
440)             # locale settings.  TODO: find a way anyway.
441) 
442)     def test_214_export_settings_no_stored_settings(
443)         self, monkeypatch: Any,
444)     ) -> None:
445)         runner = click.testing.CliRunner(mix_stderr=False)
446)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
447)                                    config={'services': {}}):
448)             try:
449)                 os.remove(cli._config_filename())
450)             except FileNotFoundError:  # pragma: no cover
451)                 pass
452)             result = runner.invoke(cli.derivepassphrase, ['--export', '-'],
453)                                    catch_exceptions=False)
454)             assert (result.exit_code, result.stderr_bytes) == (0, b''), (
455)                 'program exited with failure'
456)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

499)     def test_214c_export_settings_target_not_a_file(
500)         self, monkeypatch: Any,
501)     ) -> None:
502)         runner = click.testing.CliRunner(mix_stderr=False)
503)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
504)                                    config={'services': {}}):
505)             dname = os.path.dirname(cli._config_filename())
506)             result = runner.invoke(cli.derivepassphrase,
507)                                    ['--export', dname],
508)                                    input=b'null', catch_exceptions=False)
509)             assert result.exit_code > 0, (
510)                 'program unexpectedly succeeded'
511)             )
512)             assert result.stderr_bytes, (
513)                 'program did not print any error message'
514)             )
515)             assert b'cannot write config' in result.stderr_bytes, (
516)                 'program did not print the expected error message'
517)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

521) 
522) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
523) contents go here
524) '''
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

525)         runner = click.testing.CliRunner(mix_stderr=False)
526)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
527)                                    config={'global': {'phrase': 'abc'},
528)                                            'services': {}}):
529)             monkeypatch.setattr(click, 'edit',
530)                                 lambda *a, **kw: edit_result)
531)             result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'],
532)                                    catch_exceptions=False)
533)             assert (result.exit_code, result.stderr_bytes) == (0, b''), (
534)                 'program exited with failure'
535)             )
536)             with open(cli._config_filename(), 'rt') as infile:
537)                 config = json.load(infile)
538)             assert config == {'global': {'phrase': 'abc'},
539)                               'services': {'sv': {'notes':
540)                                                       'contents go here'}}}
541) 
542)     def test_221_edit_notes_noop(self, monkeypatch: Any) -> None:
543)         runner = click.testing.CliRunner(mix_stderr=False)
544)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
545)                                    config={'global': {'phrase': 'abc'},
546)                                            'services': {}}):
547)             monkeypatch.setattr(click, 'edit', lambda *a, **kw: None)
548)             result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'],
549)                                    catch_exceptions=False)
550)             assert (result.exit_code, result.stderr_bytes) == (0, b''), (
551)                 'program exited with failure'
552)             )
553)             with open(cli._config_filename(), 'rt') as infile:
554)                 config = json.load(infile)
555)             assert config == {'global': {'phrase': 'abc'}, 'services': {}}
556) 
557)     def test_222_edit_notes_marker_removed(self, monkeypatch: Any) -> None:
558)         runner = click.testing.CliRunner(mix_stderr=False)
559)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
560)                                    config={'global': {'phrase': 'abc'},
561)                                            'services': {}}):
562)             monkeypatch.setattr(click, 'edit', lambda *a, **kw: 'long\ntext')
563)             result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'],
564)                                    catch_exceptions=False)
565)             assert (result.exit_code, result.stderr_bytes) == (0, b''), (
566)                 'program exited with failure'
567)             )
568)             with open(cli._config_filename(), 'rt') as infile:
569)                 config = json.load(infile)
570)             assert config == {'global': {'phrase': 'abc'},
571)                               'services': {'sv': {'notes': 'long\ntext'}}}
572) 
573)     def test_223_edit_notes_abort(self, monkeypatch: Any) -> None:
574)         runner = click.testing.CliRunner(mix_stderr=False)
575)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
576)                                    config={'global': {'phrase': 'abc'},
577)                                            'services': {}}):
578)             monkeypatch.setattr(click, 'edit', lambda *a, **kw: '\n\n')
579)             result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'],
580)                                    catch_exceptions=False)
581)             assert result.exit_code != 0, 'program unexpectedly succeeded'
582)             assert b'user aborted request' in result.stderr_bytes, (
583)                 'expected error message missing'
584)             )
585)             with open(cli._config_filename(), 'rt') as infile:
586)                 config = json.load(infile)
587)             assert config == {'global': {'phrase': 'abc'}, 'services': {}}
588) 
589)     @pytest.mark.parametrize(['command_line', 'input', 'result_config'], [
590)         (
591)             ['--phrase'],
592)             b'my passphrase\n',
593)             {'global': {'phrase': 'my passphrase'}, 'services': {}},
594)         ),
595)         (
596)             ['--key'],
597)             b'1\n',
598)             {'global': {'key': DUMMY_KEY1_B64}, 'services': {}},
599)         ),
600)         (
601)             ['--phrase', 'sv'],
602)             b'my passphrase\n',
603)             {'global': {'phrase': 'abc'},
604)              'services': {'sv': {'phrase': 'my passphrase'}}},
605)         ),
606)         (
607)             ['--key', 'sv'],
608)             b'1\n',
609)             {'global': {'phrase': 'abc'},
610)              'services': {'sv': {'key': DUMMY_KEY1_B64}}},
611)         ),
612)         (
613)             ['--key', '--length', '15', 'sv'],
614)             b'1\n',
615)             {'global': {'phrase': 'abc'},
616)              'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}}},
617)         ),
618)     ])
619)     def test_224_store_config_good(
620)         self, monkeypatch: Any, command_line: list[str], input: bytes,
621)         result_config: Any,
622)     ) -> None:
623)         runner = click.testing.CliRunner(mix_stderr=False)
624)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
625)                                    config={'global': {'phrase': 'abc'},
626)                                            'services': {}}):
627)             monkeypatch.setattr(cli, '_get_suitable_ssh_keys',
628)                                 tests.suitable_ssh_keys)
629)             result = runner.invoke(cli.derivepassphrase,
630)                                    ['--config'] + command_line,
631)                                    catch_exceptions=False, input=input)
632)             assert result.exit_code == 0, 'program exited with failure'
633)             with open(cli._config_filename(), 'rt') as infile:
634)                 config = json.load(infile)
635)             assert config == result_config, (
636)                 'stored config does not match expectation'
637)             )
638) 
639)     @pytest.mark.parametrize(['command_line', 'input', 'err_text'], [
640)         ([], b'', b'cannot update global settings without actual settings'),
641)         (
642)             ['sv'],
643)             b'',
644)             b'cannot update service settings without actual settings',
645)         ),
646)         (['--phrase', 'sv'], b'', b'no passphrase given'),
647)         (['--key'], b'', b'no valid SSH key selected'),
648)     ])
649)     def test_225_store_config_fail(
650)         self, monkeypatch: Any, command_line: list[str],
651)         input: bytes, err_text: str,
652)     ) -> None:
653)         runner = click.testing.CliRunner(mix_stderr=False)
654)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
655)                                    config={'global': {'phrase': 'abc'},
656)                                            'services': {}}):
657)             monkeypatch.setattr(cli, '_get_suitable_ssh_keys',
658)                                 tests.suitable_ssh_keys)
659)             result = runner.invoke(cli.derivepassphrase,
660)                                    ['--config'] + command_line,
661)                                    catch_exceptions=False, input=input)
662)             assert result.exit_code != 0, 'program unexpectedly succeeded?!'
663)             assert err_text in result.stderr_bytes, (
664)                 'expected error message missing'
665)             )
666) 
667)     def test_225a_store_config_fail_manual_no_ssh_key_selection(
668)         self, monkeypatch: Any,
669)     ) -> None:
670)         runner = click.testing.CliRunner(mix_stderr=False)
671)         with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
672)                                    config={'global': {'phrase': 'abc'},
673)                                            'services': {}}):
674)             def raiser():
675)                 raise RuntimeError('custom error message')
676)             monkeypatch.setattr(cli, '_select_ssh_key', raiser)
677)             result = runner.invoke(cli.derivepassphrase,
678)                                    ['--key', '--config'],
679)                                    catch_exceptions=False)
680)             assert result.exit_code != 0, 'program unexpectedly succeeded'
681)             assert b'custom error message' in result.stderr_bytes, (
682)                 'expected error message missing'
683)             )
684) 
685)     def test_226_no_arguments(self) -> None:
686)         runner = click.testing.CliRunner(mix_stderr=False)
687)         result = runner.invoke(cli.derivepassphrase, [],
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

688)                                catch_exceptions=False)
689)         assert result.exit_code != 0, 'program unexpectedly succeeded'
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

691)             'expected error message missing'
692)         )
693) 
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

697)                                catch_exceptions=False)
698)         assert result.exit_code != 0, 'program unexpectedly succeeded'
Marco Ricci Rename and regroup all test...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

700)             'expected error message missing'
701)         )
702) 
703)