Marco Ricci commited on 2024-10-08 09:32:00
Zeige 5 geänderte Dateien mit 719 Einfügungen und 236 Löschungen.
The original vault(1) sometimes checks only for falsy values (in the JavaScript sense) for its configuration settings. `derivepassphrase` however uses strict type and value checks, and rejects falsy values of the wrong type. This behavior is a visible deviation from vault(1), and shall thus be removed. A new function, `_types.clean_up_falsy_vault_config_values`, normalizes falsy values in a vault configuration to their correct types, in-place. Running this on a potential vault configuration and then calling `_types.is_vault_config` should return the same validity results as vault(1) does. The new handling of falsy values invalidates most of the tests for validation errors, as `None`/`null` was a common way to generate an invalid setting. Instead, keep a master list of vault configurations that is used (perhaps filtered first) for all validation tests, and test the handling of falsy values by generating vault configurations with falsy value replacements from the master list (a custom `hypothesis` strategy). On that note, the existing `_types.validate_vault_config` has proved rather difficult to keep at 100% coverage with the new example vault configurations, because some of the error conditions are triggered elsewhere. Accordingly, instead of treating global and service-specific settings separately and quasi-duplicating all validation checks, unify them into a queue of settings dicts to check, only mildly adjusting for the very few differing keys between them. GitHub: Closes #17.
... | ... |
@@ -7,6 +7,7 @@ |
7 | 7 |
from __future__ import annotations |
8 | 8 |
|
9 | 9 |
import enum |
10 |
+import math |
|
10 | 11 |
from typing import TYPE_CHECKING |
11 | 12 |
|
12 | 13 |
from typing_extensions import ( |
... | ... |
@@ -162,8 +163,24 @@ def validate_vault_config( # noqa: C901,PLR0912,PLR0915 |
162 | 163 |
|
163 | 164 |
""" |
164 | 165 |
|
166 |
+ def maybe_quote(x: str) -> str: |
|
167 |
+ chars = ( |
|
168 |
+ frozenset('abcdefghijklmnopqrstuvwxyz') |
|
169 |
+ | frozenset('ABCDEFGHIJKLMNOPQRSTUVWXYZ') |
|
170 |
+ | frozenset('0123456789') |
|
171 |
+ | frozenset('_') |
|
172 |
+ ) |
|
173 |
+ initial = ( |
|
174 |
+ frozenset('abcdefghijklmnopqrstuvwxyz') |
|
175 |
+ | frozenset('ABCDEFGHIJKLMNOPQRSTUVWXYZ') |
|
176 |
+ | frozenset('_') |
|
177 |
+ ) |
|
178 |
+ return ( |
|
179 |
+ x if x and set(x).issubset(chars) and x[:1] in initial else repr(x) |
|
180 |
+ ) |
|
181 |
+ |
|
165 | 182 |
def as_json_path_string(json_path: Sequence[str], /) -> str: |
166 |
- return ''.join('.' + repr(x) for x in json_path) |
|
183 |
+ return ''.join('.' + maybe_quote(x) for x in json_path) |
|
167 | 184 |
|
168 | 185 |
err_obj_not_a_dict = 'vault config is not a dict' |
169 | 186 |
err_non_str_service_name = ( |
... | ... |
@@ -212,24 +229,12 @@ def validate_vault_config( # noqa: C901,PLR0912,PLR0915 |
212 | 229 |
|
213 | 230 |
if not isinstance(obj, dict): |
214 | 231 |
raise TypeError(err_obj_not_a_dict) |
232 |
+ queue_to_check: list[tuple[dict[str, Any], tuple[str, ...]]] = [] |
|
215 | 233 |
if 'global' in obj: |
216 | 234 |
o_global = obj['global'] |
217 | 235 |
if not isinstance(o_global, dict): |
218 | 236 |
raise TypeError(err_not_a_dict(['global'])) |
219 |
- for key, value in o_global.items(): |
|
220 |
- # Use match/case here once Python 3.9 becomes unsupported. |
|
221 |
- if key in {'key', 'phrase'}: |
|
222 |
- if not isinstance(value, str): |
|
223 |
- raise TypeError(err_not_a_dict(['global', key])) |
|
224 |
- elif key == 'unicode_normalization_form': |
|
225 |
- if not isinstance(value, str): |
|
226 |
- raise TypeError(err_not_a_dict(['global', key])) |
|
227 |
- if not allow_derivepassphrase_extensions: |
|
228 |
- raise ValueError( |
|
229 |
- err_derivepassphrase_extension(key, ('global',)) |
|
230 |
- ) |
|
231 |
- elif not allow_unknown_settings: |
|
232 |
- raise ValueError(err_unknown_setting(key, ('global',))) |
|
237 |
+ queue_to_check.append((o_global, ('global',))) |
|
233 | 238 |
if not isinstance(obj.get('services'), dict): |
234 | 239 |
raise TypeError(err_not_a_dict(['services'])) |
235 | 240 |
for sv_name, service in obj['services'].items(): |
... | ... |
@@ -237,23 +242,27 @@ def validate_vault_config( # noqa: C901,PLR0912,PLR0915 |
237 | 242 |
raise TypeError(err_non_str_service_name.format(sv_name)) |
238 | 243 |
if not isinstance(service, dict): |
239 | 244 |
raise TypeError(err_not_a_dict(['services', sv_name])) |
240 |
- for key, value in service.items(): |
|
245 |
+ queue_to_check.append((service, ('services', sv_name))) |
|
246 |
+ for settings, path in queue_to_check: |
|
247 |
+ for key, value in settings.items(): |
|
241 | 248 |
# Use match/case here once Python 3.9 becomes unsupported. |
242 |
- if key in {'notes', 'phrase', 'key'}: |
|
249 |
+ if key in {'key', 'phrase'}: |
|
243 | 250 |
if not isinstance(value, str): |
244 |
- raise TypeError( |
|
245 |
- err_not_a_string(['services', sv_name, key]) |
|
246 |
- ) |
|
251 |
+ raise TypeError(err_not_a_string((*path, key))) |
|
252 |
+ elif key == 'unicode_normalization_form' and path == ('global',): |
|
253 |
+ if not isinstance(value, str): |
|
254 |
+ raise TypeError(err_not_a_string((*path, key))) |
|
255 |
+ if not allow_derivepassphrase_extensions: |
|
256 |
+ raise ValueError(err_derivepassphrase_extension(key, path)) |
|
257 |
+ elif key == 'notes' and path != ('global',): |
|
258 |
+ if not isinstance(value, str): |
|
259 |
+ raise TypeError(err_not_a_string((*path, key))) |
|
247 | 260 |
elif key == 'length': |
248 | 261 |
if not isinstance(value, int): |
249 |
- raise TypeError(err_not_an_int(['services', sv_name, key])) |
|
262 |
+ raise TypeError(err_not_an_int((*path, key))) |
|
250 | 263 |
if value < 1: |
251 | 264 |
raise ValueError( |
252 |
- err_bad_number( |
|
253 |
- key, |
|
254 |
- ['services', sv_name], |
|
255 |
- strictly_positive=True, |
|
256 |
- ) |
|
265 |
+ err_bad_number(key, path, strictly_positive=True) |
|
257 | 266 |
) |
258 | 267 |
elif key in { |
259 | 268 |
'repeat', |
... | ... |
@@ -265,19 +274,13 @@ def validate_vault_config( # noqa: C901,PLR0912,PLR0915 |
265 | 274 |
'symbol', |
266 | 275 |
}: |
267 | 276 |
if not isinstance(value, int): |
268 |
- raise TypeError(err_not_an_int(['services', sv_name, key])) |
|
277 |
+ raise TypeError(err_not_an_int((*path, key))) |
|
269 | 278 |
if value < 0: |
270 | 279 |
raise ValueError( |
271 |
- err_bad_number( |
|
272 |
- key, |
|
273 |
- ['services', sv_name], |
|
274 |
- strictly_positive=False, |
|
275 |
- ) |
|
280 |
+ err_bad_number(key, path, strictly_positive=False) |
|
276 | 281 |
) |
277 | 282 |
elif not allow_unknown_settings: |
278 |
- raise ValueError( |
|
279 |
- err_unknown_setting(key, ['services', sv_name]) |
|
280 |
- ) |
|
283 |
+ raise ValueError(err_unknown_setting(key, path)) |
|
281 | 284 |
|
282 | 285 |
|
283 | 286 |
def is_vault_config(obj: Any) -> TypeIs[VaultConfig]: # noqa: ANN401 |
... | ... |
@@ -303,6 +306,99 @@ def is_vault_config(obj: Any) -> TypeIs[VaultConfig]: # noqa: ANN401 |
303 | 306 |
return True |
304 | 307 |
|
305 | 308 |
|
309 |
+def js_truthiness(value: Any, /) -> bool: # noqa: ANN401 |
|
310 |
+ """Return the truthiness of the value, according to JavaScript/ECMAScript. |
|
311 |
+ |
|
312 |
+ Like Python, ECMAScript considers certain values to be false in |
|
313 |
+ a boolean context, and every other value to be true. These |
|
314 |
+ considerations do not agree: ECMAScript considers [`math.nan`][] to |
|
315 |
+ be false too, and empty arrays and objects/dicts to be true, |
|
316 |
+ contrary to Python. Because of these discrepancies, we cannot defer |
|
317 |
+ to [`bool`][] for ECMAScript truthiness checking, and need |
|
318 |
+ a separate, explicit predicate. |
|
319 |
+ |
|
320 |
+ (Some falsy values in ECMAScript aren't defined in Python: |
|
321 |
+ `undefined`, and `document.all`. We do not implement support for |
|
322 |
+ those.) |
|
323 |
+ |
|
324 |
+ !!! note |
|
325 |
+ |
|
326 |
+ We cannot use a simple `value not in falsy_values` check, |
|
327 |
+ because [`math.nan`][] behaves in annoying and obstructive ways. |
|
328 |
+ In general, `float('NaN') == float('NaN')` is false, and |
|
329 |
+ `float('NaN') != math.nan` and `math.nan != math.nan` are true. |
|
330 |
+ CPython says `float('NaN') in [math.nan]` is false, PyPy3 says |
|
331 |
+ it is true. Seemingly the only reliable and portable way to |
|
332 |
+ check for [`math.nan`][] is to use [`math.isnan`][] directly. |
|
333 |
+ |
|
334 |
+ Args: |
|
335 |
+ value: The value to test. |
|
336 |
+ |
|
337 |
+ """ # noqa: RUF002 |
|
338 |
+ try: |
|
339 |
+ if value in {None, False, 0, 0.0, ''}: |
|
340 |
+ return False |
|
341 |
+ except TypeError: |
|
342 |
+ # All falsy values are hashable, so this can't be falsy. |
|
343 |
+ return True |
|
344 |
+ return not (isinstance(value, float) and math.isnan(value)) |
|
345 |
+ |
|
346 |
+ |
|
347 |
+def clean_up_falsy_vault_config_values(obj: Any) -> None: # noqa: ANN401,C901,PLR0912 |
|
348 |
+ """Convert falsy values in a vault config to correct types, in-place. |
|
349 |
+ |
|
350 |
+ Needed for compatibility with vault(1), which sometimes uses only |
|
351 |
+ truthiness checks. |
|
352 |
+ |
|
353 |
+ If vault(1) considered `obj` to be valid, then after clean up, |
|
354 |
+ `obj` will be valid as per [`validate_vault_config`][]. |
|
355 |
+ |
|
356 |
+ Args: |
|
357 |
+ obj: |
|
358 |
+ A presumed valid vault configuration save for using falsy |
|
359 |
+ values of the wrong type. |
|
360 |
+ |
|
361 |
+ """ |
|
362 |
+ if ( # pragma: no cover |
|
363 |
+ not isinstance(obj, dict) |
|
364 |
+ or 'services' not in obj |
|
365 |
+ or not isinstance(obj['services'], dict) |
|
366 |
+ ): |
|
367 |
+ # config is invalid |
|
368 |
+ return |
|
369 |
+ service_objects = list(obj['services'].values()) |
|
370 |
+ if not all( # pragma: no cover |
|
371 |
+ isinstance(service_obj, dict) for service_obj in service_objects |
|
372 |
+ ): |
|
373 |
+ # config is invalid |
|
374 |
+ return |
|
375 |
+ if 'global' in obj: |
|
376 |
+ if isinstance(obj['global'], dict): |
|
377 |
+ service_objects.append(obj['global']) |
|
378 |
+ else: # pragma: no cover |
|
379 |
+ # config is invalid |
|
380 |
+ return |
|
381 |
+ for service_obj in service_objects: |
|
382 |
+ for key, value in list(service_obj.items()): |
|
383 |
+ # Use match/case here once Python 3.9 becomes unsupported. |
|
384 |
+ if key == 'phrase': |
|
385 |
+ if not js_truthiness(value): |
|
386 |
+ service_obj[key] = '' |
|
387 |
+ elif key in {'notes', 'key', 'length', 'repeat'}: |
|
388 |
+ if not js_truthiness(value): |
|
389 |
+ service_obj.pop(key) |
|
390 |
+ elif key in { # noqa: SIM102 |
|
391 |
+ 'lower', |
|
392 |
+ 'upper', |
|
393 |
+ 'number', |
|
394 |
+ 'space', |
|
395 |
+ 'dash', |
|
396 |
+ 'symbol', |
|
397 |
+ }: |
|
398 |
+ if not js_truthiness(value) and value != 0: |
|
399 |
+ service_obj.pop(key) |
|
400 |
+ |
|
401 |
+ |
|
306 | 402 |
class KeyCommentPair(NamedTuple): |
307 | 403 |
"""SSH key plus comment pair. For typing purposes. |
308 | 404 |
|
... | ... |
@@ -1410,6 +1410,7 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
1410 | 1410 |
err(f'Cannot load config: cannot decode JSON: {e}') |
1411 | 1411 |
except OSError as e: |
1412 | 1412 |
err(f'Cannot load config: {e.strerror}: {e.filename!r}') |
1413 |
+ _types.clean_up_falsy_vault_config_values(maybe_config) |
|
1413 | 1414 |
if _types.is_vault_config(maybe_config): |
1414 | 1415 |
form = cast( |
1415 | 1416 |
Literal['NFC', 'NFD', 'NFKC', 'NFKD'], |
... | ... |
@@ -6,6 +6,7 @@ from __future__ import annotations |
6 | 6 |
|
7 | 7 |
import base64 |
8 | 8 |
import contextlib |
9 |
+import copy |
|
9 | 10 |
import enum |
10 | 11 |
import importlib.util |
11 | 12 |
import json |
... | ... |
@@ -16,7 +17,9 @@ import tempfile |
16 | 17 |
import zipfile |
17 | 18 |
from typing import TYPE_CHECKING |
18 | 19 |
|
20 |
+import hypothesis |
|
19 | 21 |
import pytest |
22 |
+from hypothesis import strategies |
|
20 | 23 |
from typing_extensions import NamedTuple, Self, assert_never |
21 | 24 |
|
22 | 25 |
from derivepassphrase import _types, cli, ssh_agent, vault |
... | ... |
@@ -38,6 +41,376 @@ if TYPE_CHECKING: |
38 | 41 |
derived_passphrase: bytes | str | None |
39 | 42 |
|
40 | 43 |
|
44 |
+class ValidationSettings(NamedTuple): |
|
45 |
+ allow_unknown_settings: bool |
|
46 |
+ allow_derivepassphrase_extensions: bool |
|
47 |
+ |
|
48 |
+ |
|
49 |
+class VaultTestConfig(NamedTuple): |
|
50 |
+ config: Any |
|
51 |
+ comment: str |
|
52 |
+ validation_settings: ValidationSettings | None |
|
53 |
+ |
|
54 |
+ |
|
55 |
+TEST_CONFIGS: list[VaultTestConfig] = [ |
|
56 |
+ VaultTestConfig(None, 'not a dict', None), |
|
57 |
+ VaultTestConfig({}, 'missing required keys', None), |
|
58 |
+ VaultTestConfig( |
|
59 |
+ {'global': None, 'services': {}}, 'bad config value: global', None |
|
60 |
+ ), |
|
61 |
+ VaultTestConfig( |
|
62 |
+ {'global': {'key': 123}, 'services': {}}, |
|
63 |
+ 'bad config value: global.key', |
|
64 |
+ None, |
|
65 |
+ ), |
|
66 |
+ VaultTestConfig( |
|
67 |
+ {'global': {'phrase': 'abc', 'key': '...'}, 'services': {}}, |
|
68 |
+ '', |
|
69 |
+ None, |
|
70 |
+ ), |
|
71 |
+ VaultTestConfig({'services': None}, 'bad config value: services', None), |
|
72 |
+ VaultTestConfig( |
|
73 |
+ {'services': {'1': {}, 2: {}}}, 'bad config value: services."2"', None |
|
74 |
+ ), |
|
75 |
+ VaultTestConfig( |
|
76 |
+ {'services': {'1': {}, '2': 2}}, 'bad config value: services."2"', None |
|
77 |
+ ), |
|
78 |
+ VaultTestConfig( |
|
79 |
+ {'services': {'sv': {'notes': ['sentinel', 'list']}}}, |
|
80 |
+ 'bad config value: services.sv.notes', |
|
81 |
+ None, |
|
82 |
+ ), |
|
83 |
+ VaultTestConfig( |
|
84 |
+ {'services': {'sv': {'notes': 'blah blah blah'}}}, '', None |
|
85 |
+ ), |
|
86 |
+ VaultTestConfig( |
|
87 |
+ {'services': {'sv': {'length': '200'}}}, |
|
88 |
+ 'bad config value: services.sv.length', |
|
89 |
+ None, |
|
90 |
+ ), |
|
91 |
+ VaultTestConfig( |
|
92 |
+ {'services': {'sv': {'length': 0.5}}}, |
|
93 |
+ 'bad config value: services.sv.length', |
|
94 |
+ None, |
|
95 |
+ ), |
|
96 |
+ VaultTestConfig( |
|
97 |
+ {'services': {'sv': {'length': ['sentinel', 'list']}}}, |
|
98 |
+ 'bad config value: services.sv.length', |
|
99 |
+ None, |
|
100 |
+ ), |
|
101 |
+ VaultTestConfig( |
|
102 |
+ {'services': {'sv': {'length': -10}}}, |
|
103 |
+ 'bad config value: services.sv.length', |
|
104 |
+ None, |
|
105 |
+ ), |
|
106 |
+ VaultTestConfig( |
|
107 |
+ {'services': {'sv': {'lower': '10'}}}, |
|
108 |
+ 'bad config value: services.sv.lower', |
|
109 |
+ None, |
|
110 |
+ ), |
|
111 |
+ VaultTestConfig( |
|
112 |
+ {'services': {'sv': {'upper': -10}}}, |
|
113 |
+ 'bad config value: services.sv.upper', |
|
114 |
+ None, |
|
115 |
+ ), |
|
116 |
+ VaultTestConfig( |
|
117 |
+ {'services': {'sv': {'number': ['sentinel', 'list']}}}, |
|
118 |
+ 'bad config value: services.sv.number', |
|
119 |
+ None, |
|
120 |
+ ), |
|
121 |
+ VaultTestConfig( |
|
122 |
+ { |
|
123 |
+ 'global': {'phrase': 'my secret phrase'}, |
|
124 |
+ 'services': {'sv': {'length': 10}}, |
|
125 |
+ }, |
|
126 |
+ '', |
|
127 |
+ None, |
|
128 |
+ ), |
|
129 |
+ VaultTestConfig( |
|
130 |
+ {'services': {'sv': {'length': 10, 'phrase': '...'}}}, '', None |
|
131 |
+ ), |
|
132 |
+ VaultTestConfig( |
|
133 |
+ {'services': {'sv': {'length': 10, 'key': '...'}}}, '', None |
|
134 |
+ ), |
|
135 |
+ VaultTestConfig( |
|
136 |
+ {'services': {'sv': {'upper': 10, 'key': '...'}}}, '', None |
|
137 |
+ ), |
|
138 |
+ VaultTestConfig( |
|
139 |
+ {'services': {'sv': {'phrase': 'abc', 'key': '...'}}}, '', None |
|
140 |
+ ), |
|
141 |
+ VaultTestConfig( |
|
142 |
+ { |
|
143 |
+ 'global': {'phrase': 'abc'}, |
|
144 |
+ 'services': {'sv': {'phrase': 'abc', 'length': 10}}, |
|
145 |
+ }, |
|
146 |
+ '', |
|
147 |
+ None, |
|
148 |
+ ), |
|
149 |
+ VaultTestConfig( |
|
150 |
+ { |
|
151 |
+ 'global': {'key': '...'}, |
|
152 |
+ 'services': {'sv': {'phrase': 'abc', 'length': 10}}, |
|
153 |
+ }, |
|
154 |
+ '', |
|
155 |
+ None, |
|
156 |
+ ), |
|
157 |
+ VaultTestConfig( |
|
158 |
+ { |
|
159 |
+ 'global': {'key': '...'}, |
|
160 |
+ 'services': { |
|
161 |
+ 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
162 |
+ 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}, |
|
163 |
+ }, |
|
164 |
+ }, |
|
165 |
+ '', |
|
166 |
+ None, |
|
167 |
+ ), |
|
168 |
+ VaultTestConfig( |
|
169 |
+ { |
|
170 |
+ 'global': {'key': '...', 'unicode_normalization_form': 'NFC'}, |
|
171 |
+ 'services': { |
|
172 |
+ 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
173 |
+ 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}, |
|
174 |
+ }, |
|
175 |
+ }, |
|
176 |
+ '', |
|
177 |
+ None, |
|
178 |
+ ), |
|
179 |
+ VaultTestConfig( |
|
180 |
+ { |
|
181 |
+ 'global': {'key': '...', 'unicode_normalization_form': True}, |
|
182 |
+ 'services': {}, |
|
183 |
+ }, |
|
184 |
+ 'bad config value: global.unicode_normalization_form', |
|
185 |
+ None, |
|
186 |
+ ), |
|
187 |
+ VaultTestConfig( |
|
188 |
+ { |
|
189 |
+ 'global': {'key': '...', 'unicode_normalization_form': 'NFC'}, |
|
190 |
+ 'services': { |
|
191 |
+ 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
192 |
+ 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}, |
|
193 |
+ }, |
|
194 |
+ }, |
|
195 |
+ '', |
|
196 |
+ ValidationSettings(False, True), |
|
197 |
+ ), |
|
198 |
+ VaultTestConfig( |
|
199 |
+ { |
|
200 |
+ 'global': {'key': '...', 'unicode_normalization_form': 'NFC'}, |
|
201 |
+ 'services': { |
|
202 |
+ 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
203 |
+ 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}, |
|
204 |
+ }, |
|
205 |
+ }, |
|
206 |
+ 'extension key: .global.unicode_normalization_form', |
|
207 |
+ ValidationSettings(False, False), |
|
208 |
+ ), |
|
209 |
+ VaultTestConfig( |
|
210 |
+ { |
|
211 |
+ 'global': {'key': '...', 'unknown_key': True}, |
|
212 |
+ 'services': { |
|
213 |
+ 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
214 |
+ 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}, |
|
215 |
+ }, |
|
216 |
+ }, |
|
217 |
+ '', |
|
218 |
+ ValidationSettings(True, False), |
|
219 |
+ ), |
|
220 |
+ VaultTestConfig( |
|
221 |
+ { |
|
222 |
+ 'global': {'key': '...', 'unknown_key': True}, |
|
223 |
+ 'services': { |
|
224 |
+ 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
225 |
+ 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}, |
|
226 |
+ }, |
|
227 |
+ }, |
|
228 |
+ 'unknown key: .global.unknown_key', |
|
229 |
+ ValidationSettings(False, False), |
|
230 |
+ ), |
|
231 |
+ VaultTestConfig( |
|
232 |
+ { |
|
233 |
+ 'global': {'key': '...'}, |
|
234 |
+ 'services': { |
|
235 |
+ 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
236 |
+ 'sv2': { |
|
237 |
+ 'length': 10, |
|
238 |
+ 'repeat': 1, |
|
239 |
+ 'lower': 1, |
|
240 |
+ 'unknown_key': True, |
|
241 |
+ }, |
|
242 |
+ }, |
|
243 |
+ }, |
|
244 |
+ 'unknown_key: .services.sv2.unknown_key', |
|
245 |
+ ValidationSettings(False, False), |
|
246 |
+ ), |
|
247 |
+ VaultTestConfig( |
|
248 |
+ { |
|
249 |
+ 'global': {'key': '...', 'unicode_normalization_form': 'NFC'}, |
|
250 |
+ 'services': { |
|
251 |
+ 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
252 |
+ 'sv2': { |
|
253 |
+ 'length': 10, |
|
254 |
+ 'repeat': 1, |
|
255 |
+ 'lower': 1, |
|
256 |
+ 'unknown_key': True, |
|
257 |
+ }, |
|
258 |
+ }, |
|
259 |
+ }, |
|
260 |
+ '', |
|
261 |
+ ValidationSettings(True, True), |
|
262 |
+ ), |
|
263 |
+ VaultTestConfig( |
|
264 |
+ { |
|
265 |
+ 'global': {'key': '...', 'unicode_normalization_form': 'NFC'}, |
|
266 |
+ 'services': { |
|
267 |
+ 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
268 |
+ 'sv2': { |
|
269 |
+ 'length': 10, |
|
270 |
+ 'repeat': 1, |
|
271 |
+ 'lower': 1, |
|
272 |
+ 'unknown_key': True, |
|
273 |
+ }, |
|
274 |
+ }, |
|
275 |
+ }, |
|
276 |
+ ( |
|
277 |
+ 'extension key (permitted): .global.unicode_normalization_form; ' |
|
278 |
+ 'unknown key: .services.sv2.unknown_key' |
|
279 |
+ ), |
|
280 |
+ ValidationSettings(False, True), |
|
281 |
+ ), |
|
282 |
+ VaultTestConfig( |
|
283 |
+ { |
|
284 |
+ 'global': {'key': '...', 'unicode_normalization_form': 'NFC'}, |
|
285 |
+ 'services': { |
|
286 |
+ 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
287 |
+ 'sv2': { |
|
288 |
+ 'length': 10, |
|
289 |
+ 'repeat': 1, |
|
290 |
+ 'lower': 1, |
|
291 |
+ 'unknown_key': True, |
|
292 |
+ }, |
|
293 |
+ }, |
|
294 |
+ }, |
|
295 |
+ ( |
|
296 |
+ 'unknown key (permitted): .services.sv2.unknown_key; ' |
|
297 |
+ 'extension key: .global.unicode_normalization_form' |
|
298 |
+ ), |
|
299 |
+ ValidationSettings(True, False), |
|
300 |
+ ), |
|
301 |
+] |
|
302 |
+ |
|
303 |
+ |
|
304 |
+def is_valid_test_config(conf: VaultTestConfig, /) -> bool: |
|
305 |
+ """Return true if the test config is valid. |
|
306 |
+ |
|
307 |
+ Args: |
|
308 |
+ conf: The test config to check. |
|
309 |
+ |
|
310 |
+ """ |
|
311 |
+ return not conf.comment and conf.validation_settings in { |
|
312 |
+ None, |
|
313 |
+ (True, True), |
|
314 |
+ } |
|
315 |
+ |
|
316 |
+ |
|
317 |
+def _test_config_ids(val: VaultTestConfig) -> Any: # pragma: no cover |
|
318 |
+ """pytest id function for VaultTestConfig objects.""" |
|
319 |
+ assert isinstance(val, VaultTestConfig) |
|
320 |
+ return val[1] or (val[0], val[1], val[2]) |
|
321 |
+ |
|
322 |
+ |
|
323 |
+def is_smudgable_vault_test_config(conf: VaultTestConfig) -> bool: |
|
324 |
+ """Check whether this vault test config can be effectively smudged. |
|
325 |
+ |
|
326 |
+ A "smudged" test config is one where falsy values (in the JavaScript |
|
327 |
+ sense) can be replaced by other falsy values without changing the |
|
328 |
+ meaning of the config. |
|
329 |
+ |
|
330 |
+ Args: |
|
331 |
+ conf: A test config to check. |
|
332 |
+ |
|
333 |
+ Returns: |
|
334 |
+ True if the test config can be smudged, False otherwise. |
|
335 |
+ |
|
336 |
+ """ |
|
337 |
+ config = conf.config |
|
338 |
+ return bool( |
|
339 |
+ isinstance(config, dict) |
|
340 |
+ and ('global' not in config or isinstance(config['global'], dict)) |
|
341 |
+ and ('services' in config and isinstance(config['services'], dict)) |
|
342 |
+ and all(isinstance(x, dict) for x in config['services'].values()) |
|
343 |
+ and (config['services'] or config.get('global')) |
|
344 |
+ ) |
|
345 |
+ |
|
346 |
+ |
|
347 |
+@strategies.composite |
|
348 |
+def smudged_vault_test_config( |
|
349 |
+ draw: strategies.DrawFn, |
|
350 |
+ config: Any = strategies.sampled_from(TEST_CONFIGS).filter( # noqa: B008 |
|
351 |
+ is_smudgable_vault_test_config |
|
352 |
+ ), |
|
353 |
+) -> Any: |
|
354 |
+ """Hypothesis strategy to replace falsy values with other falsy values. |
|
355 |
+ |
|
356 |
+ Uses [`_types.js_truthiness`][] internally, which is tested |
|
357 |
+ separately by |
|
358 |
+ [`tests.test_derivepassphrase_types.test_100_js_truthiness`][]. |
|
359 |
+ |
|
360 |
+ Args: |
|
361 |
+ draw: |
|
362 |
+ The hypothesis draw function. |
|
363 |
+ config: |
|
364 |
+ A strategy which generates [`VaultTestConfig`][] objects. |
|
365 |
+ |
|
366 |
+ Returns: |
|
367 |
+ A new [`VaultTestConfig`][] where some falsy values have been |
|
368 |
+ replaced or added. |
|
369 |
+ |
|
370 |
+ """ |
|
371 |
+ |
|
372 |
+ falsy = (None, False, 0, 0.0, '', float('nan')) |
|
373 |
+ falsy_no_str = (None, False, 0, 0.0, float('nan')) |
|
374 |
+ falsy_no_zero = (None, False, '', float('nan')) |
|
375 |
+ conf = draw(config) |
|
376 |
+ hypothesis.assume(is_smudgable_vault_test_config(conf)) |
|
377 |
+ obj = copy.deepcopy(conf.config) |
|
378 |
+ services: list[dict[str, Any]] = list(obj['services'].values()) |
|
379 |
+ if 'global' in obj: |
|
380 |
+ services.append(obj['global']) |
|
381 |
+ assert all(isinstance(x, dict) for x in services), ( |
|
382 |
+ 'is_smudgable_vault_test_config guard failed to ' |
|
383 |
+ 'ensure each setings dict is a dict' |
|
384 |
+ ) |
|
385 |
+ for service in services: |
|
386 |
+ for key in ('phrase',): |
|
387 |
+ value = service.get(key) |
|
388 |
+ if not _types.js_truthiness(value) and value != '': |
|
389 |
+ service[key] = draw(strategies.sampled_from(falsy_no_str)) |
|
390 |
+ for key in ( |
|
391 |
+ 'notes', |
|
392 |
+ 'key', |
|
393 |
+ 'length', |
|
394 |
+ 'repeat', |
|
395 |
+ ): |
|
396 |
+ value = service.get(key) |
|
397 |
+ if not _types.js_truthiness(value): |
|
398 |
+ service[key] = draw(strategies.sampled_from(falsy)) |
|
399 |
+ for key in ( |
|
400 |
+ 'lower', |
|
401 |
+ 'upper', |
|
402 |
+ 'number', |
|
403 |
+ 'space', |
|
404 |
+ 'dash', |
|
405 |
+ 'symbol', |
|
406 |
+ ): |
|
407 |
+ value = service.get(key) |
|
408 |
+ if not _types.js_truthiness(value) and value != 0: |
|
409 |
+ service[key] = draw(strategies.sampled_from(falsy_no_zero)) |
|
410 |
+ hypothesis.assume(obj != conf.config) |
|
411 |
+ return VaultTestConfig(obj, conf.comment, conf.validation_settings) |
|
412 |
+ |
|
413 |
+ |
|
41 | 414 |
class KnownSSHAgent(str, enum.Enum): |
42 | 415 |
UNKNOWN: str = '(unknown)' |
43 | 416 |
Pageant: str = 'Pageant' |
... | ... |
@@ -5,6 +5,7 @@ |
5 | 5 |
from __future__ import annotations |
6 | 6 |
|
7 | 7 |
import contextlib |
8 |
+import copy |
|
8 | 9 |
import errno |
9 | 10 |
import json |
10 | 11 |
import os |
... | ... |
@@ -13,7 +14,9 @@ import socket |
13 | 14 |
from typing import TYPE_CHECKING |
14 | 15 |
|
15 | 16 |
import click.testing |
17 |
+import hypothesis |
|
16 | 18 |
import pytest |
19 |
+from hypothesis import strategies |
|
17 | 20 |
from typing_extensions import NamedTuple |
18 | 21 |
|
19 | 22 |
import tests |
... | ... |
@@ -39,6 +42,8 @@ DUMMY_KEY2_B64 = tests.DUMMY_KEY2_B64 |
39 | 42 |
DUMMY_KEY3 = tests.DUMMY_KEY3 |
40 | 43 |
DUMMY_KEY3_B64 = tests.DUMMY_KEY3_B64 |
41 | 44 |
|
45 |
+TEST_CONFIGS = tests.TEST_CONFIGS |
|
46 |
+ |
|
42 | 47 |
|
43 | 48 |
class IncompatibleConfiguration(NamedTuple): |
44 | 49 |
other_options: list[tuple[str, ...]] |
... | ... |
@@ -573,7 +578,74 @@ class TestCLI: |
573 | 578 |
error='mutually exclusive with ' |
574 | 579 |
), 'expected error exit and known error message' |
575 | 580 |
|
576 |
- def test_213_import_bad_config_not_vault_config( |
|
581 |
+ @pytest.mark.parametrize( |
|
582 |
+ 'config', |
|
583 |
+ [ |
|
584 |
+ conf.config |
|
585 |
+ for conf in tests.TEST_CONFIGS |
|
586 |
+ if tests.is_valid_test_config(conf) |
|
587 |
+ ], |
|
588 |
+ ) |
|
589 |
+ def test_213_import_config_success( |
|
590 |
+ self, |
|
591 |
+ monkeypatch: pytest.MonkeyPatch, |
|
592 |
+ config: Any, |
|
593 |
+ ) -> None: |
|
594 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
595 |
+ with tests.isolated_vault_config( |
|
596 |
+ monkeypatch=monkeypatch, |
|
597 |
+ runner=runner, |
|
598 |
+ config={'services': {}}, |
|
599 |
+ ): |
|
600 |
+ _result = runner.invoke( |
|
601 |
+ cli.derivepassphrase_vault, |
|
602 |
+ ['--import', '-'], |
|
603 |
+ input=json.dumps(config), |
|
604 |
+ catch_exceptions=False, |
|
605 |
+ ) |
|
606 |
+ with open( |
|
607 |
+ cli._config_filename(subsystem='vault'), encoding='UTF-8' |
|
608 |
+ ) as infile: |
|
609 |
+ config2 = json.load(infile) |
|
610 |
+ result = tests.ReadableResult.parse(_result) |
|
611 |
+ assert result.clean_exit(empty_stderr=True), 'expected clean exit' |
|
612 |
+ assert config2 == config, 'config not imported correctly' |
|
613 |
+ |
|
614 |
+ @hypothesis.given( |
|
615 |
+ conf=tests.smudged_vault_test_config( |
|
616 |
+ strategies.sampled_from(TEST_CONFIGS).filter( |
|
617 |
+ tests.is_valid_test_config |
|
618 |
+ ) |
|
619 |
+ ) |
|
620 |
+ ) |
|
621 |
+ def test_213a_import_config_success( |
|
622 |
+ self, |
|
623 |
+ conf: tests.VaultTestConfig, |
|
624 |
+ ) -> None: |
|
625 |
+ config = conf.config |
|
626 |
+ config2 = copy.deepcopy(config) |
|
627 |
+ _types.clean_up_falsy_vault_config_values(config2) |
|
628 |
+ runner = click.testing.CliRunner(mix_stderr=False) |
|
629 |
+ with tests.isolated_vault_config( |
|
630 |
+ monkeypatch=pytest.MonkeyPatch(), |
|
631 |
+ runner=runner, |
|
632 |
+ config={'services': {}}, |
|
633 |
+ ): |
|
634 |
+ _result = runner.invoke( |
|
635 |
+ cli.derivepassphrase_vault, |
|
636 |
+ ['--import', '-'], |
|
637 |
+ input=json.dumps(config), |
|
638 |
+ catch_exceptions=False, |
|
639 |
+ ) |
|
640 |
+ with open( |
|
641 |
+ cli._config_filename(subsystem='vault'), encoding='UTF-8' |
|
642 |
+ ) as infile: |
|
643 |
+ config3 = json.load(infile) |
|
644 |
+ result = tests.ReadableResult.parse(_result) |
|
645 |
+ assert result.clean_exit(empty_stderr=True), 'expected clean exit' |
|
646 |
+ assert config3 == config2, 'config not imported correctly' |
|
647 |
+ |
|
648 |
+ def test_213b_import_bad_config_not_vault_config( |
|
577 | 649 |
self, |
578 | 650 |
monkeypatch: pytest.MonkeyPatch, |
579 | 651 |
) -> None: |
... | ... |
@@ -590,7 +662,7 @@ class TestCLI: |
590 | 662 |
error='Invalid vault config' |
591 | 663 |
), 'expected error exit and known error message' |
592 | 664 |
|
593 |
- def test_213a_import_bad_config_not_json_data( |
|
665 |
+ def test_213c_import_bad_config_not_json_data( |
|
594 | 666 |
self, |
595 | 667 |
monkeypatch: pytest.MonkeyPatch, |
596 | 668 |
) -> None: |
... | ... |
@@ -607,7 +679,7 @@ class TestCLI: |
607 | 679 |
error='cannot decode JSON' |
608 | 680 |
), 'expected error exit and known error message' |
609 | 681 |
|
610 |
- def test_213b_import_bad_config_not_a_file( |
|
682 |
+ def test_213d_import_bad_config_not_a_file( |
|
611 | 683 |
self, |
612 | 684 |
monkeypatch: pytest.MonkeyPatch, |
613 | 685 |
) -> None: |
... | ... |
@@ -4,136 +4,103 @@ |
4 | 4 |
|
5 | 5 |
from __future__ import annotations |
6 | 6 |
|
7 |
+import copy |
|
8 |
+ |
|
9 |
+import hypothesis |
|
7 | 10 |
import pytest |
11 |
+from hypothesis import strategies |
|
8 | 12 |
from typing_extensions import Any |
9 | 13 |
|
14 |
+import tests |
|
10 | 15 |
from derivepassphrase import _types |
11 | 16 |
|
12 | 17 |
|
18 |
+@hypothesis.given( |
|
19 |
+ value=strategies.one_of( |
|
20 |
+ strategies.recursive( |
|
21 |
+ strategies.one_of( |
|
22 |
+ strategies.none(), |
|
23 |
+ strategies.booleans(), |
|
24 |
+ strategies.integers(), |
|
25 |
+ strategies.floats(allow_nan=False, allow_infinity=False), |
|
26 |
+ strategies.text(max_size=100), |
|
27 |
+ strategies.binary(max_size=100), |
|
28 |
+ ), |
|
29 |
+ lambda s: strategies.one_of( |
|
30 |
+ strategies.frozensets(s, max_size=100), |
|
31 |
+ strategies.builds( |
|
32 |
+ tuple, strategies.frozensets(s, max_size=100) |
|
33 |
+ ), |
|
34 |
+ ), |
|
35 |
+ max_leaves=8, |
|
36 |
+ ), |
|
37 |
+ strategies.recursive( |
|
38 |
+ strategies.one_of( |
|
39 |
+ strategies.none(), |
|
40 |
+ strategies.booleans(), |
|
41 |
+ strategies.integers(), |
|
42 |
+ strategies.floats(allow_nan=False, allow_infinity=False), |
|
43 |
+ strategies.text(max_size=100), |
|
44 |
+ strategies.binary(max_size=100), |
|
45 |
+ ), |
|
46 |
+ lambda s: strategies.one_of( |
|
47 |
+ strategies.lists(s, max_size=100), |
|
48 |
+ strategies.dictionaries(strategies.text(max_size=100), s), |
|
49 |
+ ), |
|
50 |
+ max_leaves=25, |
|
51 |
+ ), |
|
52 |
+ strategies.builds(tuple), |
|
53 |
+ strategies.builds(list), |
|
54 |
+ strategies.builds(dict), |
|
55 |
+ strategies.builds(set), |
|
56 |
+ strategies.builds(frozenset), |
|
57 |
+ ), |
|
58 |
+) |
|
59 |
+def test_100_js_truthiness(value: Any) -> None: |
|
60 |
+ expected = ( |
|
61 |
+ value is not None # noqa: PLR1714 |
|
62 |
+ and value != False # noqa: E712 |
|
63 |
+ and value != 0 |
|
64 |
+ and value != 0.0 |
|
65 |
+ and value != '' |
|
66 |
+ ) |
|
67 |
+ assert _types.js_truthiness(value) == expected |
|
68 |
+ |
|
69 |
+ |
|
13 | 70 |
@pytest.mark.parametrize( |
14 |
- ['obj', 'comment'], |
|
71 |
+ 'test_config', |
|
15 | 72 |
[ |
16 |
- (None, 'not a dict'), |
|
17 |
- ({}, 'missing required keys'), |
|
18 |
- ({'global': None, 'services': {}}, 'bad config value: global'), |
|
19 |
- ( |
|
20 |
- {'global': {'key': 123}, 'services': {}}, |
|
21 |
- 'bad config value: global.key', |
|
22 |
- ), |
|
23 |
- ( |
|
24 |
- {'global': {'phrase': 'abc', 'key': '...'}, 'services': {}}, |
|
25 |
- '', |
|
26 |
- ), |
|
27 |
- ({'services': None}, 'bad config value: services'), |
|
28 |
- ({'services': {2: {}}}, 'bad config value: services."2"'), |
|
29 |
- ({'services': {'2': 2}}, 'bad config value: services."2"'), |
|
30 |
- ( |
|
31 |
- {'services': {'sv': {'notes': False}}}, |
|
32 |
- 'bad config value: services.sv.notes', |
|
33 |
- ), |
|
34 |
- ({'services': {'sv': {'notes': 'blah blah blah'}}}, ''), |
|
35 |
- ( |
|
36 |
- {'services': {'sv': {'length': '200'}}}, |
|
37 |
- 'bad config value: services.sv.length', |
|
38 |
- ), |
|
39 |
- ( |
|
40 |
- {'services': {'sv': {'length': 0.5}}}, |
|
41 |
- 'bad config value: services.sv.length', |
|
42 |
- ), |
|
43 |
- ( |
|
44 |
- {'services': {'sv': {'length': -10}}}, |
|
45 |
- 'bad config value: services.sv.length', |
|
46 |
- ), |
|
47 |
- ( |
|
48 |
- {'services': {'sv': {'lower': '10'}}}, |
|
49 |
- 'bad config value: services.sv.lower', |
|
50 |
- ), |
|
51 |
- ( |
|
52 |
- {'services': {'sv': {'upper': -10}}}, |
|
53 |
- 'bad config value: services.sv.upper', |
|
54 |
- ), |
|
55 |
- ( |
|
56 |
- { |
|
57 |
- 'global': {'phrase': 'my secret phrase'}, |
|
58 |
- 'services': {'sv': {'length': 10}}, |
|
59 |
- }, |
|
60 |
- '', |
|
61 |
- ), |
|
62 |
- ({'services': {'sv': {'length': 10, 'phrase': '...'}}}, ''), |
|
63 |
- ({'services': {'sv': {'length': 10, 'key': '...'}}}, ''), |
|
64 |
- ({'services': {'sv': {'upper': 10, 'key': '...'}}}, ''), |
|
65 |
- ({'services': {'sv': {'phrase': 'abc', 'key': '...'}}}, ''), |
|
66 |
- ( |
|
67 |
- { |
|
68 |
- 'global': {'phrase': 'abc'}, |
|
69 |
- 'services': {'sv': {'phrase': 'abc', 'length': 10}}, |
|
70 |
- }, |
|
71 |
- '', |
|
72 |
- ), |
|
73 |
- ( |
|
74 |
- { |
|
75 |
- 'global': {'key': '...'}, |
|
76 |
- 'services': {'sv': {'phrase': 'abc', 'length': 10}}, |
|
77 |
- }, |
|
78 |
- '', |
|
79 |
- ), |
|
80 |
- ( |
|
81 |
- { |
|
82 |
- 'global': {'key': '...'}, |
|
83 |
- 'services': { |
|
84 |
- 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
85 |
- 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}, |
|
86 |
- }, |
|
87 |
- }, |
|
88 |
- '', |
|
89 |
- ), |
|
90 |
- ( |
|
91 |
- { |
|
92 |
- 'global': {'key': '...', 'unicode_normalization_form': 'NFC'}, |
|
93 |
- 'services': { |
|
94 |
- 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
95 |
- 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}, |
|
96 |
- }, |
|
97 |
- }, |
|
98 |
- '', |
|
99 |
- ), |
|
100 |
- ( |
|
101 |
- { |
|
102 |
- 'global': {'key': '...', 'unicode_normalization_form': None}, |
|
103 |
- 'services': {}, |
|
104 |
- }, |
|
105 |
- 'bad config value: global.unicode_normalization_form', |
|
106 |
- ), |
|
107 |
- ( |
|
108 |
- { |
|
109 |
- 'global': {'key': '...', 'unknown_key': None}, |
|
110 |
- 'services': { |
|
111 |
- 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
112 |
- 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}, |
|
113 |
- }, |
|
114 |
- }, |
|
115 |
- '', |
|
116 |
- ), |
|
117 |
- ( |
|
118 |
- { |
|
119 |
- 'global': {'key': '...', 'unicode_normalization_form': 'NFC'}, |
|
120 |
- 'services': { |
|
121 |
- 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
122 |
- 'sv2': { |
|
123 |
- 'length': 10, |
|
124 |
- 'repeat': 1, |
|
125 |
- 'lower': 1, |
|
126 |
- 'unknown_key': None, |
|
127 |
- }, |
|
128 |
- }, |
|
129 |
- }, |
|
130 |
- '', |
|
131 |
- ), |
|
73 |
+ conf |
|
74 |
+ for conf in tests.TEST_CONFIGS |
|
75 |
+ if conf.validation_settings in {None, (True, True)} |
|
132 | 76 |
], |
77 |
+ ids=tests._test_config_ids, |
|
133 | 78 |
) |
134 |
-def test_200_is_vault_config(obj: Any, comment: str) -> None: |
|
135 |
- is_vault_config = _types.is_vault_config |
|
136 |
- assert is_vault_config(obj) == (not comment), ( |
|
79 |
+def test_200_is_vault_config(test_config: tests.VaultTestConfig) -> None: |
|
80 |
+ obj, comment, _ = test_config |
|
81 |
+ obj = copy.deepcopy(obj) |
|
82 |
+ _types.clean_up_falsy_vault_config_values(obj) |
|
83 |
+ assert _types.is_vault_config(obj) == (not comment), ( |
|
84 |
+ 'failed to complain about: ' + comment |
|
85 |
+ if comment |
|
86 |
+ else 'failed on valid example' |
|
87 |
+ ) |
|
88 |
+ |
|
89 |
+ |
|
90 |
+@hypothesis.given( |
|
91 |
+ test_config=tests.smudged_vault_test_config( |
|
92 |
+ config=strategies.sampled_from(tests.TEST_CONFIGS).filter( |
|
93 |
+ tests.is_valid_test_config |
|
94 |
+ ) |
|
95 |
+ ) |
|
96 |
+) |
|
97 |
+def test_200a_is_vault_config_smudged( |
|
98 |
+ test_config: tests.VaultTestConfig, |
|
99 |
+) -> None: |
|
100 |
+ obj, comment, _ = test_config |
|
101 |
+ obj = copy.deepcopy(obj) |
|
102 |
+ _types.clean_up_falsy_vault_config_values(obj) |
|
103 |
+ assert _types.is_vault_config(obj) == (not comment), ( |
|
137 | 104 |
'failed to complain about: ' + comment |
138 | 105 |
if comment |
139 | 106 |
else 'failed on valid example' |
... | ... |
@@ -141,88 +108,62 @@ def test_200_is_vault_config(obj: Any, comment: str) -> None: |
141 | 108 |
|
142 | 109 |
|
143 | 110 |
@pytest.mark.parametrize( |
144 |
- ['obj', 'allow_unknown_settings', 'allow_derivepassphrase_extensions'], |
|
145 |
- [ |
|
146 |
- ( |
|
147 |
- { |
|
148 |
- 'global': {'key': '...', 'unicode_normalization_form': 'NFC'}, |
|
149 |
- 'services': { |
|
150 |
- 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
151 |
- 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}, |
|
152 |
- }, |
|
153 |
- }, |
|
154 |
- False, |
|
155 |
- False, |
|
156 |
- ), |
|
157 |
- ( |
|
158 |
- { |
|
159 |
- 'global': {'key': '...', 'unknown_key': None}, |
|
160 |
- 'services': { |
|
161 |
- 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
162 |
- 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}, |
|
163 |
- }, |
|
164 |
- }, |
|
165 |
- False, |
|
166 |
- False, |
|
167 |
- ), |
|
168 |
- ( |
|
169 |
- { |
|
170 |
- 'global': {'key': '...', 'unicode_normalization_form': 'NFC'}, |
|
171 |
- 'services': { |
|
172 |
- 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
173 |
- 'sv2': { |
|
174 |
- 'length': 10, |
|
175 |
- 'repeat': 1, |
|
176 |
- 'lower': 1, |
|
177 |
- 'unknown_key': None, |
|
178 |
- }, |
|
179 |
- }, |
|
180 |
- }, |
|
181 |
- False, |
|
182 |
- False, |
|
183 |
- ), |
|
184 |
- ( |
|
185 |
- { |
|
186 |
- 'global': {'key': '...', 'unicode_normalization_form': 'NFC'}, |
|
187 |
- 'services': { |
|
188 |
- 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
189 |
- 'sv2': { |
|
190 |
- 'length': 10, |
|
191 |
- 'repeat': 1, |
|
192 |
- 'lower': 1, |
|
193 |
- 'unknown_key': None, |
|
194 |
- }, |
|
195 |
- }, |
|
196 |
- }, |
|
197 |
- False, |
|
198 |
- True, |
|
199 |
- ), |
|
200 |
- ( |
|
201 |
- { |
|
202 |
- 'global': {'key': '...', 'unicode_normalization_form': 'NFC'}, |
|
203 |
- 'services': { |
|
204 |
- 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
205 |
- 'sv2': { |
|
206 |
- 'length': 10, |
|
207 |
- 'repeat': 1, |
|
208 |
- 'lower': 1, |
|
209 |
- 'unknown_key': None, |
|
210 |
- }, |
|
211 |
- }, |
|
212 |
- }, |
|
213 |
- True, |
|
214 |
- False, |
|
215 |
- ), |
|
216 |
- ], |
|
111 |
+ 'test_config', tests.TEST_CONFIGS, ids=tests._test_config_ids |
|
112 |
+) |
|
113 |
+def test_400_validate_vault_config(test_config: tests.VaultTestConfig) -> None: |
|
114 |
+ obj, comment, validation_settings = test_config |
|
115 |
+ allow_unknown_settings, allow_derivepassphrase_extensions = ( |
|
116 |
+ validation_settings or (True, True) |
|
117 |
+ ) |
|
118 |
+ obj = copy.deepcopy(obj) |
|
119 |
+ _types.clean_up_falsy_vault_config_values(obj) |
|
120 |
+ if comment: |
|
121 |
+ with pytest.raises((TypeError, ValueError)): |
|
122 |
+ _types.validate_vault_config( |
|
123 |
+ obj, |
|
124 |
+ allow_unknown_settings=allow_unknown_settings, |
|
125 |
+ allow_derivepassphrase_extensions=allow_derivepassphrase_extensions, |
|
126 |
+ ) |
|
127 |
+ else: |
|
128 |
+ try: |
|
129 |
+ _types.validate_vault_config( |
|
130 |
+ obj, |
|
131 |
+ allow_unknown_settings=allow_unknown_settings, |
|
132 |
+ allow_derivepassphrase_extensions=allow_derivepassphrase_extensions, |
|
133 |
+ ) |
|
134 |
+ except (TypeError, ValueError) as exc: # pragma: no cover |
|
135 |
+ assert not exc, 'failed to validate valid example' # noqa: PT017 |
|
136 |
+ |
|
137 |
+ |
|
138 |
+@hypothesis.given( |
|
139 |
+ test_config=tests.smudged_vault_test_config( |
|
140 |
+ config=strategies.sampled_from(tests.TEST_CONFIGS).filter( |
|
141 |
+ tests.is_smudgable_vault_test_config |
|
142 |
+ ) |
|
217 | 143 |
) |
218 |
-def test_400_validate_vault_config( |
|
219 |
- obj: Any, |
|
220 |
- allow_unknown_settings: bool, |
|
221 |
- allow_derivepassphrase_extensions: bool, |
|
144 |
+) |
|
145 |
+def test_400a_validate_vault_config_smudged( |
|
146 |
+ test_config: tests.VaultTestConfig, |
|
222 | 147 |
) -> None: |
223 |
- with pytest.raises((TypeError, ValueError), match='vault config '): |
|
148 |
+ obj, comment, validation_settings = test_config |
|
149 |
+ allow_unknown_settings, allow_derivepassphrase_extensions = ( |
|
150 |
+ validation_settings or (True, True) |
|
151 |
+ ) |
|
152 |
+ obj = copy.deepcopy(obj) |
|
153 |
+ _types.clean_up_falsy_vault_config_values(obj) |
|
154 |
+ if comment: |
|
155 |
+ with pytest.raises((TypeError, ValueError)): |
|
156 |
+ _types.validate_vault_config( |
|
157 |
+ obj, |
|
158 |
+ allow_unknown_settings=allow_unknown_settings, |
|
159 |
+ allow_derivepassphrase_extensions=allow_derivepassphrase_extensions, |
|
160 |
+ ) |
|
161 |
+ else: |
|
162 |
+ try: |
|
224 | 163 |
_types.validate_vault_config( |
225 | 164 |
obj, |
226 | 165 |
allow_unknown_settings=allow_unknown_settings, |
227 | 166 |
allow_derivepassphrase_extensions=allow_derivepassphrase_extensions, |
228 | 167 |
) |
168 |
+ except (TypeError, ValueError) as exc: # pragma: no cover |
|
169 |
+ assert not exc, 'failed to validate valid example' # noqa: PT017 |
|
229 | 170 |