Add finished command-line i...
Marco Ricci authored 2 months ago
|
22) DUMMY_CONFIG_SETTINGS = {"length": 10, "upper": 1, "lower": 1, "repeat": 5,
23) "number": 1, "space": 1, "dash": 1, "symbol": 1}
24) DUMMY_RESULT_PASSPHRASE = b'.2V_QJkd o'
25) DUMMY_RESULT_KEY1 = b'E<b<{ -7iG'
26) DUMMY_PHRASE_FROM_KEY1_RAW = (
27) b'\x00\x00\x00\x0bssh-ed25519'
28) b'\x00\x00\x00@\xf0\x98\x19\x80l\x1a\x97\xd5&\x03n'
29) b'\xcc\xe3e\x8f\x86f\x07\x13\x19\x13\t!33\xf9\xe46S'
30) b'\x1d\xaf\xfd\r\x08\x1f\xec\xf8s\x9b\x8c_U9\x16|ST,'
31) b'\x1eR\xbb0\xed\x7f\x89\xe2/iQU\xd8\x9e\xa6\x02'
32) )
33) DUMMY_PHRASE_FROM_KEY1 = b'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg=='
34)
35) # See Ed25519 and RSA test keys in test_key_signing.py
36) DUMMY_KEY1 = 'AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2'
37) DUMMY_KEY2 = 'AAAAB3NzaC1yc2EAAAADAQABAAABgQCxoe7pezhxWy4NI0mUwKqg9WCYOAS+IjxN9eYcqpfcmQiojcuy9XsiN/xYJ1O94SrsKS5mEia2xHnYA4RUChTyYNcM2v6cnnBQ/N/VQhpGMN7SVxdbhKUXTWFCwbjBgO6rGyHB6WtoH8vd7TOEPt+NgcXwhsWyoaUUdYTA62V+GF9vEmxMaC4ubgDz+B0QkPnauSoNxmkhcIe0lsLNb1pClZyz88PDnKXCX/d0HuN/HJ+sbPg7dCvOyqFYSyKn3uY6bCXqoIdurxXzH3O7z0P8f5sbmKOrGGKNuNxVRbeVl/D/3uDL0nqsbfUc1qvkfwbJwtMXC4IV6kOZMSk2BAsqh7x48gQ+rhYeEVSi8F3CWs4HJQoqrGt7K9a3mCSlMBHP70u3w6ME7eumoryxlUofewTd17ZEkzdX08l2ZlKzZvwQUrc+xQZ2Uw8z2mfW6Ti4gi0pYGaig7Ke4PwuXpo/C5YAWfeXycsvJZ2uaYRjMdZeJGNAnHLUGLkBscw5aI8='
38)
39)
40) class IncompatibleConfiguration(NamedTuple):
41) other_options: list[tuple[str, ...]]
42) needs_service: bool | None
43) input: bytes | None
44)
45) class SingleConfiguration(NamedTuple):
46) needs_service: bool | None
47) input: bytes | None
48) check_success: bool
49)
50) class OptionCombination(NamedTuple):
51) options: list[str]
52) incompatible: bool
53) needs_service: bool | None
54) input: bytes | None
55) check_success: bool
56)
57) PASSWORD_GENERATION_OPTIONS: list[tuple[str, ...]] = [
58) ('--phrase',), ('--key',), ('--length', '20'), ('--repeat', '20'),
59) ('--lower', '1'), ('--upper', '1'), ('--number', '1'),
60) ('--space', '1'), ('--dash', '1'), ('--symbol', '1')
61) ]
62) CONFIGURATION_OPTIONS: list[tuple[str, ...]] = [
63) ('--notes',), ('--config',), ('--delete',), ('--delete-globals',),
64) ('--clear',)
65) ]
66) CONFIGURATION_COMMANDS: list[tuple[str, ...]] = [
67) ('--notes',), ('--delete',), ('--delete-globals',), ('--clear',)
68) ]
69) STORAGE_OPTIONS: list[tuple[str, ...]] = [
70) ('--export', '-'), ('--import', '-')
71) ]
72) INCOMPATIBLE: dict[tuple[str, ...], IncompatibleConfiguration] = {
73) ('--phrase',): IncompatibleConfiguration(
74) [('--key',)] + CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
75) True, DUMMY_PASSPHRASE),
76) ('--key',): IncompatibleConfiguration(
77) CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
78) True, DUMMY_PASSPHRASE),
79) ('--length', '20'): IncompatibleConfiguration(
80) CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
81) True, DUMMY_PASSPHRASE),
82) ('--repeat', '20'): IncompatibleConfiguration(
83) CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
84) True, DUMMY_PASSPHRASE),
85) ('--lower', '1'): IncompatibleConfiguration(
86) CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
87) True, DUMMY_PASSPHRASE),
88) ('--upper', '1'): IncompatibleConfiguration(
89) CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
90) True, DUMMY_PASSPHRASE),
91) ('--number', '1'): IncompatibleConfiguration(
92) CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
93) True, DUMMY_PASSPHRASE),
94) ('--space', '1'): IncompatibleConfiguration(
95) CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
96) True, DUMMY_PASSPHRASE),
97) ('--dash', '1'): IncompatibleConfiguration(
98) CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
99) True, DUMMY_PASSPHRASE),
100) ('--symbol', '1'): IncompatibleConfiguration(
101) CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
102) True, DUMMY_PASSPHRASE),
103) ('--notes',): IncompatibleConfiguration(
104) [('--config',), ('--delete',), ('--delete-globals',),
105) ('--clear',)] + STORAGE_OPTIONS,
106) True, None),
107) ('--config', '-p'): IncompatibleConfiguration(
108) [('--delete',), ('--delete-globals',),
109) ('--clear',)] + STORAGE_OPTIONS,
110) None, DUMMY_PASSPHRASE),
111) ('--delete',): IncompatibleConfiguration(
112) [('--delete-globals',), ('--clear',)] + STORAGE_OPTIONS, True, None),
113) ('--delete-globals',): IncompatibleConfiguration(
114) [('--clear',)] + STORAGE_OPTIONS, False, None),
115) ('--clear',): IncompatibleConfiguration(STORAGE_OPTIONS, False, None),
116) ('--export', '-'): IncompatibleConfiguration(
117) [('--import', '-')], False, None),
118) ('--import', '-'): IncompatibleConfiguration(
119) [], False, None),
120) }
121) SINGLES: dict[tuple[str, ...], SingleConfiguration] = {
122) ('--phrase',): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
123) ('--key',): SingleConfiguration(True, None, False),
124) ('--length', '20'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
125) ('--repeat', '20'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
126) ('--lower', '1'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
127) ('--upper', '1'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
128) ('--number', '1'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
129) ('--space', '1'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
130) ('--dash', '1'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
131) ('--symbol', '1'): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
132) ('--notes',): SingleConfiguration(True, None, False),
133) ('--config', '-p'): SingleConfiguration(None, DUMMY_PASSPHRASE, False),
134) ('--delete',): SingleConfiguration(True, None, False),
135) ('--delete-globals',): SingleConfiguration(False, None, True),
136) ('--clear',): SingleConfiguration(False, None, True),
137) ('--export', '-'): SingleConfiguration(False, None, True),
138) ('--import', '-'): SingleConfiguration(False, b'{"services": {}}', True),
139) }
140) INTERESTING_OPTION_COMBINATIONS: list[OptionCombination] = []
141) config: OptionCombination | SingleConfiguration
142) for opt, config in INCOMPATIBLE.items():
143) for opt2 in config.other_options:
144) INTERESTING_OPTION_COMBINATIONS.extend([
145) OptionCombination(options=list(opt + opt2), incompatible=True,
146) needs_service=config.needs_service,
147) input=config.input, check_success=False),
148) OptionCombination(options=list(opt2 + opt), incompatible=True,
149) needs_service=config.needs_service,
150) input=config.input, check_success=False)
151) ])
152) for opt, config in SINGLES.items():
153) INTERESTING_OPTION_COMBINATIONS.append(
154) OptionCombination(options=list(opt), incompatible=False,
155) needs_service=config.needs_service,
156) input=config.input,
157) check_success=config.check_success))
158)
159) @contextlib.contextmanager
160) def isolated_config(
161) monkeypatch: Any, runner: click.testing.CliRunner, config: Any,
162) ):
163) with runner.isolated_filesystem():
164) monkeypatch.setenv('HOME', os.getcwd())
165) monkeypatch.setenv('USERPROFILE', os.getcwd())
166) monkeypatch.delenv(cli.prog_name.replace(' ', '_').upper() + '_PATH',
167) raising=False)
168) os.makedirs(os.path.dirname(cli._config_filename()), exist_ok=True)
169) with open(cli._config_filename(), 'wt') as outfile:
170) json.dump(config, outfile)
171) yield
172)
173)
174) def test_100_save_bad_config(monkeypatch: Any) -> None:
175) runner = click.testing.CliRunner()
176) with isolated_config(monkeypatch=monkeypatch, runner=runner, config={}):
177) with pytest.raises(ValueError, match='Invalid vault config'):
178) cli._save_config(None) # type: ignore
179)
180)
181) def test_101_prompt_for_selection_multiple(monkeypatch: Any) -> None:
182) @click.command()
183) @click.option('--heading', default='Our menu:')
184) @click.argument('items', nargs=-1)
185) def driver(heading, items):
186) # from https://montypython.fandom.com/wiki/Spam#The_menu
187) items = items or [
188) 'Egg and bacon',
189) 'Egg, sausage and bacon',
190) 'Egg and spam',
191) 'Egg, bacon and spam',
192) 'Egg, bacon, sausage and spam',
193) 'Spam, bacon, sausage and spam',
194) 'Spam, egg, spam, spam, bacon and spam',
195) 'Spam, spam, spam, egg and spam',
196) ('Spam, spam, spam, spam, spam, spam, baked beans, '
197) 'spam, spam, spam and spam'),
198) ('Lobster thermidor aux crevettes with a mornay sauce '
199) 'garnished with truffle paté, brandy '
200) 'and a fried egg on top and spam'),
201) ]
202) index = cli._prompt_for_selection(items, heading=heading)
203) click.echo('A fine choice: ', nl=False)
204) click.echo(items[index])
205) click.echo('(Note: Vikings strictly optional.)')
206) runner = click.testing.CliRunner(mix_stderr=True)
207) result = runner.invoke(driver, [], input='9')
208) assert result.exit_code == 0, 'driver program failed'
209) assert result.stdout == '''\
210) Our menu:
211) [1] Egg and bacon
212) [2] Egg, sausage and bacon
213) [3] Egg and spam
214) [4] Egg, bacon and spam
215) [5] Egg, bacon, sausage and spam
216) [6] Spam, bacon, sausage and spam
217) [7] Spam, egg, spam, spam, bacon and spam
218) [8] Spam, spam, spam, egg and spam
219) [9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
220) [10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam
221) Your selection? (1-10, leave empty to abort): 9
222) A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
223) (Note: Vikings strictly optional.)
224) ''', 'driver program produced unexpected output'
225) result = runner.invoke(driver, ['--heading='], input='',
226) catch_exceptions=True)
227) assert result.exit_code > 0, 'driver program succeeded?!'
228) assert result.stdout == '''\
229) [1] Egg and bacon
230) [2] Egg, sausage and bacon
231) [3] Egg and spam
232) [4] Egg, bacon and spam
233) [5] Egg, bacon, sausage and spam
234) [6] Spam, bacon, sausage and spam
235) [7] Spam, egg, spam, spam, bacon and spam
236) [8] Spam, spam, spam, egg and spam
237) [9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
238) [10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam
239) Your selection? (1-10, leave empty to abort): \n''', (
240) 'driver program produced unexpected output'
241) )
242) assert isinstance(result.exception, IndexError), (
243) 'driver program did not raise IndexError?!'
244) )
245)
246)
247) def test_102_prompt_for_selection_single(monkeypatch: Any) -> None:
248) @click.command()
249) @click.option('--item', default='baked beans')
250) @click.argument('prompt')
251) def driver(item, prompt):
252) try:
253) cli._prompt_for_selection([item], heading='',
254) single_choice_prompt=prompt)
255) except IndexError as e:
256) click.echo('Boo.')
257) raise e
258) else:
259) click.echo('Great!')
260) runner = click.testing.CliRunner(mix_stderr=True)
261) result = runner.invoke(driver, ['Will replace with spam. Confirm, y/n?'],
262) input='y')
263) assert result.exit_code == 0, 'driver program failed'
264) assert result.stdout == '''\
265) [1] baked beans
266) Will replace with spam. Confirm, y/n? y
267) Great!
268) ''', 'driver program produced unexpected output'
269) result = runner.invoke(driver,
270) ['Will replace with spam, okay? ' +
271) '(Please say "y" or "n".)'],
272) input='')
273) assert result.exit_code > 0, 'driver program succeeded?!'
274) assert result.stdout == '''\
275) [1] baked beans
276) Will replace with spam, okay? (Please say "y" or "n".):
277) Boo.
278) ''', 'driver program produced unexpected output'
279) assert isinstance(result.exception, IndexError), (
280) 'driver program did not raise IndexError?!'
281) )
282)
283)
284) def test_103_prompt_for_passphrase(monkeypatch: Any) -> None:
285) monkeypatch.setattr(click, 'prompt',
286) lambda *a, **kw: json.dumps({'args': a, 'kwargs': kw}))
287) res = json.loads(cli._prompt_for_passphrase())
288) assert 'args' in res and 'kwargs' in res, (
289) 'missing arguments to passphrase prompt'
290) )
291) assert res['args'][:1] == ['Passphrase'], (
292) 'missing arguments to passphrase prompt'
293) )
294) assert (res['kwargs'].get('default') == ''
295) and not res['kwargs'].get('show_default', True)), (
296) 'missing arguments to passphrase prompt'
297) )
298) assert res['kwargs'].get('err') and res['kwargs'].get('hide_input'), (
299) 'missing arguments to passphrase prompt'
300) )
301)
|