Marco Ricci commited on 2024-12-19 15:04:11
              Zeige 2 geänderte Dateien mit 247 Einfügungen und 31 Löschungen.
            
Instead of storing Unicode normalization preferences in the vault
configuration file, store these settings in the user configuration file:
  * For a service `SERVICE`, consult the configuration key
    `vault.SERVICE.unicode-normalization-form`.
  * If that is unset, or if there is no service, use
    `vault.default-unicode-normalization-form`.
  * If that too is unset, default to `NFC`.
                  | ... | ... | 
                      @@ -1247,25 +1247,69 @@ def _prompt_for_passphrase() -> str:  | 
                  
| 1247 | 1247 | 
                        )  | 
                    
| 1248 | 1248 | 
                         | 
                    
| 1249 | 1249 | 
                         | 
                    
| 1250 | 
                        +def _toml_key(*parts: str) -> str:  | 
                    |
| 1251 | 
                        + """Return a formatted TOML key, given its parts."""  | 
                    |
| 1252 | 
                        + def escape(string: str) -> str:  | 
                    |
| 1253 | 
                        +        translated = string.translate({
                       | 
                    |
| 1254 | 
                        + 0: r'\u0000',  | 
                    |
| 1255 | 
                        + 1: r'\u0001',  | 
                    |
| 1256 | 
                        + 2: r'\u0002',  | 
                    |
| 1257 | 
                        + 3: r'\u0003',  | 
                    |
| 1258 | 
                        + 4: r'\u0004',  | 
                    |
| 1259 | 
                        + 5: r'\u0005',  | 
                    |
| 1260 | 
                        + 6: r'\u0006',  | 
                    |
| 1261 | 
                        + 7: r'\u0007',  | 
                    |
| 1262 | 
                        + 8: r'\b',  | 
                    |
| 1263 | 
                        + 9: r'\t',  | 
                    |
| 1264 | 
                        + 10: r'\n',  | 
                    |
| 1265 | 
                        + 11: r'\u000B',  | 
                    |
| 1266 | 
                        + 12: r'\f',  | 
                    |
| 1267 | 
                        + 13: r'\r',  | 
                    |
| 1268 | 
                        + 14: r'\u000E',  | 
                    |
| 1269 | 
                        + 15: r'\u000F',  | 
                    |
| 1270 | 
                        +            ord('"'): r'\"',
                       | 
                    |
| 1271 | 
                        +            ord('\\'): r'\\',
                       | 
                    |
| 1272 | 
                        + 127: r'\u007F',  | 
                    |
| 1273 | 
                        + })  | 
                    |
| 1274 | 
                        +        return f'"{translated}"' if translated != string else string
                       | 
                    |
| 1275 | 
                        + return '.'.join(map(escape, parts))  | 
                    |
| 1276 | 
                        +  | 
                    |
| 1277 | 
                        +  | 
                    |
| 1250 | 1278 | 
                        class _ORIGIN(enum.Enum):  | 
                    
| 1251 | 
                        - INTERACTIVE: str = 'interactive'  | 
                    |
| 1279 | 
                        + INTERACTIVE: str = 'interactive input'  | 
                    |
| 1252 | 1280 | 
                         | 
                    
| 1253 | 1281 | 
                         | 
                    
| 1254 | 1282 | 
                        def _check_for_misleading_passphrase(  | 
                    
| 1255 | 1283 | 
                        key: tuple[str, ...] | _ORIGIN,  | 
                    
| 1256 | 1284 | 
                        value: dict[str, Any],  | 
                    
| 1257 | 1285 | 
                        *,  | 
                    
| 1258 | 
                        - form: Literal['NFC', 'NFD', 'NFKC', 'NFKD'] = 'NFC',  | 
                    |
| 1286 | 
                        + main_config: dict[str, Any],  | 
                    |
| 1259 | 1287 | 
                        ) -> None:  | 
                    
| 1288 | 
                        + form_key = 'unicode-normalization-form'  | 
                    |
| 1289 | 
                        +    default_form: str = main_config.get('vault', {}).get(
                       | 
                    |
| 1290 | 
                        +        f'default-{form_key}', 'NFC'
                       | 
                    |
| 1291 | 
                        + )  | 
                    |
| 1292 | 
                        +    form_dict: dict[str, dict] = main_config.get('vault', {}).get(form_key, {})
                       | 
                    |
| 1293 | 
                        + form: Any = (  | 
                    |
| 1294 | 
                        + default_form  | 
                    |
| 1295 | 
                        +        if isinstance(key, _ORIGIN) or key == ('global',)
                       | 
                    |
| 1296 | 
                        + else form_dict.get(key[1], default_form)  | 
                    |
| 1297 | 
                        + )  | 
                    |
| 1298 | 
                        + config_key = (  | 
                    |
| 1299 | 
                        +        _toml_key('vault', key[1], form_key)
                       | 
                    |
| 1300 | 
                        + if isinstance(key, tuple) and len(key) > 1 and key[1] in form_dict  | 
                    |
| 1301 | 
                        +        else f'vault.default-{form_key}'
                       | 
                    |
| 1302 | 
                        + )  | 
                    |
| 1303 | 
                        +    if form not in {'NFC', 'NFD', 'NFKC', 'NFKD'}:
                       | 
                    |
| 1304 | 
                        +        msg = f'Invalid value {form!r} for config key {config_key}'
                       | 
                    |
| 1305 | 
                        + raise AssertionError(msg)  | 
                    |
| 1260 | 1306 | 
                        logger = logging.getLogger(PROG_NAME)  | 
                    
| 1307 | 
                        + formatted_key = (  | 
                    |
| 1308 | 
                        + key.value if isinstance(key, _ORIGIN) else _types.json_path(key)  | 
                    |
| 1309 | 
                        + )  | 
                    |
| 1261 | 1310 | 
                        if 'phrase' in value:  | 
                    
| 1262 | 1311 | 
                        phrase = value['phrase']  | 
                    
| 1263 | 1312 | 
                        if not unicodedata.is_normalized(form, phrase):  | 
                    
| 1264 | 
                        - formatted_key = (  | 
                    |
| 1265 | 
                        - key.value  | 
                    |
| 1266 | 
                        - if isinstance(key, _ORIGIN)  | 
                    |
| 1267 | 
                        - else _types.json_path(key)  | 
                    |
| 1268 | 
                        - )  | 
                    |
| 1269 | 1313 | 
                        logger.warning(  | 
                    
| 1270 | 1314 | 
                        (  | 
                    
| 1271 | 1315 | 
                        'the %s passphrase is not %s-normalized. '  | 
                    
| ... | ... | 
                      @@ -1274,6 +1318,7 @@ def _check_for_misleading_passphrase(  | 
                  
| 1274 | 1318 | 
                        ),  | 
                    
| 1275 | 1319 | 
                        formatted_key,  | 
                    
| 1276 | 1320 | 
                        form,  | 
                    
| 1321 | 
                        + stacklevel=2,  | 
                    |
| 1277 | 1322 | 
                        )  | 
                    
| 1278 | 1323 | 
                         | 
                    
| 1279 | 1324 | 
                         | 
                    
| ... | ... | 
                      @@ -1952,24 +1997,20 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915  | 
                  
| 1952 | 1997 | 
                        ),  | 
                    
| 1953 | 1998 | 
                        PROG_NAME,  | 
                    
| 1954 | 1999 | 
                        )  | 
                    
| 1955 | 
                        - form = cast(  | 
                    |
| 1956 | 
                        - Literal['NFC', 'NFD', 'NFKC', 'NFKD'],  | 
                    |
| 1957 | 
                        -            maybe_config.get('global', {}).get(
                       | 
                    |
| 1958 | 
                        - 'unicode_normalization_form', 'NFC'  | 
                    |
| 1959 | 
                        - ),  | 
                    |
| 1960 | 
                        - )  | 
                    |
| 1961 | 
                        -        assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
                       | 
                    |
| 2000 | 
                        + try:  | 
                    |
| 1962 | 2001 | 
                        _check_for_misleading_passphrase(  | 
                    
| 1963 | 2002 | 
                                         ('global',),
                       | 
                    
| 1964 | 2003 | 
                                         cast(dict[str, Any], maybe_config.get('global', {})),
                       | 
                    
| 1965 | 
                        - form=form,  | 
                    |
| 2004 | 
                        + main_config=user_config,  | 
                    |
| 1966 | 2005 | 
                        )  | 
                    
| 1967 | 2006 | 
                        for key, value in maybe_config['services'].items():  | 
                    
| 1968 | 2007 | 
                        _check_for_misleading_passphrase(  | 
                    
| 1969 | 2008 | 
                                             ('services', key),
                       | 
                    
| 1970 | 2009 | 
                        cast(dict[str, Any], value),  | 
                    
| 1971 | 
                        - form=form,  | 
                    |
| 2010 | 
                        + main_config=user_config,  | 
                    |
| 1972 | 2011 | 
                        )  | 
                    
| 2012 | 
                        + except AssertionError as e:  | 
                    |
| 2013 | 
                        +            err('The configuration file is invalid.  ' + str(e))
                       | 
                    |
| 1973 | 2014 | 
                        if overwrite_config:  | 
                    
| 1974 | 2015 | 
                        put_config(maybe_config)  | 
                    
| 1975 | 2016 | 
                        else:  | 
                    
| ... | ... | 
                      @@ -2081,10 +2122,14 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915  | 
                  
| 2081 | 2122 | 
                        elif use_phrase:  | 
                    
| 2082 | 2123 | 
                        view['phrase'] = phrase  | 
                    
| 2083 | 2124 | 
                        settings_type = 'service' if service else 'global'  | 
                    
| 2125 | 
                        + try:  | 
                    |
| 2084 | 2126 | 
                        _check_for_misleading_passphrase(  | 
                    
| 2085 | 2127 | 
                                                 ('services', service) if service else ('global',),
                       | 
                    
| 2086 | 2128 | 
                                                 {'phrase': phrase},
                       | 
                    
| 2129 | 
                        + main_config=user_config,  | 
                    |
| 2087 | 2130 | 
                        )  | 
                    
| 2131 | 
                        + except AssertionError as e:  | 
                    |
| 2132 | 
                        +                    err('The configuration file is invalid.  ' + str(e))
                       | 
                    |
| 2088 | 2133 | 
                        if 'key' in settings:  | 
                    
| 2089 | 2134 | 
                        logger.warning(  | 
                    
| 2090 | 2135 | 
                        (  | 
                    
| ... | ... | 
                      @@ -2123,16 +2168,14 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915  | 
                  
| 2123 | 2168 | 
                        }  | 
                    
| 2124 | 2169 | 
                         | 
                    
| 2125 | 2170 | 
                        if use_phrase:  | 
                    
| 2126 | 
                        - form = cast(  | 
                    |
| 2127 | 
                        - Literal['NFC', 'NFD', 'NFKC', 'NFKD'],  | 
                    |
| 2128 | 
                        -                    configuration.get('global', {}).get(
                       | 
                    |
| 2129 | 
                        - 'unicode_normalization_form', 'NFC'  | 
                    |
| 2130 | 
                        - ),  | 
                    |
| 2131 | 
                        - )  | 
                    |
| 2132 | 
                        -                assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
                       | 
                    |
| 2171 | 
                        + try:  | 
                    |
| 2133 | 2172 | 
                        _check_for_misleading_passphrase(  | 
                    
| 2134 | 
                        -                    _ORIGIN.INTERACTIVE, {'phrase': phrase}, form=form
                       | 
                    |
| 2173 | 
                        + _ORIGIN.INTERACTIVE,  | 
                    |
| 2174 | 
                        +                        {'phrase': phrase},
                       | 
                    |
| 2175 | 
                        + main_config=user_config,  | 
                    |
| 2135 | 2176 | 
                        )  | 
                    
| 2177 | 
                        + except AssertionError as e:  | 
                    |
| 2178 | 
                        +                    err('The configuration file is invalid.  ' + str(e))
                       | 
                    |
| 2136 | 2179 | 
                         | 
                    
| 2137 | 2180 | 
                        # If either --key or --phrase are given, use that setting.  | 
                    
| 2138 | 2181 | 
                        # Otherwise, if both key and phrase are set in the config,  | 
                    
| ... | ... | 
                      @@ -12,6 +12,7 @@ import logging  | 
                  
| 12 | 12 | 
                        import os  | 
                    
| 13 | 13 | 
                        import shutil  | 
                    
| 14 | 14 | 
                        import socket  | 
                    
| 15 | 
                        +import textwrap  | 
                    |
| 15 | 16 | 
                        import warnings  | 
                    
| 16 | 17 | 
                        from typing import TYPE_CHECKING  | 
                    
| 17 | 18 | 
                         | 
                    
| ... | ... | 
                      @@ -1414,9 +1415,10 @@ contents go here  | 
                  
| 1414 | 1415 | 
                        ), 'expected error exit and known error message'  | 
                    
| 1415 | 1416 | 
                         | 
                    
| 1416 | 1417 | 
                        @pytest.mark.parametrize(  | 
                    
| 1417 | 
                        - ['command_line', 'input', 'warning_message'],  | 
                    |
| 1418 | 
                        + ['main_config', 'command_line', 'input', 'warning_message'],  | 
                    |
| 1418 | 1419 | 
                        [  | 
                    
| 1419 | 1420 | 
                        pytest.param(  | 
                    
| 1421 | 
                        + '',  | 
                    |
| 1420 | 1422 | 
                        ['--import', '-'],  | 
                    
| 1421 | 1423 | 
                                         json.dumps({
                       | 
                    
| 1422 | 1424 | 
                                             'global': {'phrase': 'Du\u0308sseldorf'},
                       | 
                    
| ... | ... | 
                      @@ -1426,6 +1428,7 @@ contents go here  | 
                  
| 1426 | 1428 | 
                        id='global-NFC',  | 
                    
| 1427 | 1429 | 
                        ),  | 
                    
| 1428 | 1430 | 
                        pytest.param(  | 
                    
| 1431 | 
                        + '',  | 
                    |
| 1429 | 1432 | 
                        ['--import', '-'],  | 
                    
| 1430 | 1433 | 
                                         json.dumps({
                       | 
                    
| 1431 | 1434 | 
                                             'services': {
                       | 
                    
| ... | ... | 
                      @@ -1440,6 +1443,7 @@ contents go here  | 
                  
| 1440 | 1443 | 
                        id='service-weird-name-NFC',  | 
                    
| 1441 | 1444 | 
                        ),  | 
                    
| 1442 | 1445 | 
                        pytest.param(  | 
                    
| 1446 | 
                        + '',  | 
                    |
| 1443 | 1447 | 
                        ['--config', '-p', '--', DUMMY_SERVICE],  | 
                    
| 1444 | 1448 | 
                        'Du\u0308sseldorf',  | 
                    
| 1445 | 1449 | 
                        (  | 
                    
| ... | ... | 
                      @@ -1449,16 +1453,20 @@ contents go here  | 
                  
| 1449 | 1453 | 
                        id='config-NFC',  | 
                    
| 1450 | 1454 | 
                        ),  | 
                    
| 1451 | 1455 | 
                        pytest.param(  | 
                    
| 1456 | 
                        + '',  | 
                    |
| 1452 | 1457 | 
                        ['-p', '--', DUMMY_SERVICE],  | 
                    
| 1453 | 1458 | 
                        'Du\u0308sseldorf',  | 
                    
| 1454 | 
                        - 'the interactive passphrase is not NFC-normalized',  | 
                    |
| 1459 | 
                        + 'the interactive input passphrase is not NFC-normalized',  | 
                    |
| 1455 | 1460 | 
                        id='direct-input-NFC',  | 
                    
| 1456 | 1461 | 
                        ),  | 
                    
| 1457 | 1462 | 
                        pytest.param(  | 
                    
| 1463 | 
                        + textwrap.dedent(r"""  | 
                    |
| 1464 | 
                        + [vault]  | 
                    |
| 1465 | 
                        + default-unicode-normalization-form = 'NFD'  | 
                    |
| 1466 | 
                        + """),  | 
                    |
| 1458 | 1467 | 
                        ['--import', '-'],  | 
                    
| 1459 | 1468 | 
                                         json.dumps({
                       | 
                    
| 1460 | 1469 | 
                                             'global': {
                       | 
                    
| 1461 | 
                        - 'unicode_normalization_form': 'NFD',  | 
                    |
| 1462 | 1470 | 
                        'phrase': 'D\u00fcsseldorf',  | 
                    
| 1463 | 1471 | 
                        },  | 
                    
| 1464 | 1472 | 
                                             'services': {},
                       | 
                    
| ... | ... | 
                      @@ -1467,11 +1475,12 @@ contents go here  | 
                  
| 1467 | 1475 | 
                        id='global-NFD',  | 
                    
| 1468 | 1476 | 
                        ),  | 
                    
| 1469 | 1477 | 
                        pytest.param(  | 
                    
| 1478 | 
                        + textwrap.dedent(r"""  | 
                    |
| 1479 | 
                        + [vault]  | 
                    |
| 1480 | 
                        + default-unicode-normalization-form = 'NFD'  | 
                    |
| 1481 | 
                        + """),  | 
                    |
| 1470 | 1482 | 
                        ['--import', '-'],  | 
                    
| 1471 | 1483 | 
                                         json.dumps({
                       | 
                    
| 1472 | 
                        -                    'global': {
                       | 
                    |
| 1473 | 
                        - 'unicode_normalization_form': 'NFD',  | 
                    |
| 1474 | 
                        - },  | 
                    |
| 1475 | 1484 | 
                                             'services': {
                       | 
                    
| 1476 | 1485 | 
                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    
| 1477 | 1486 | 
                                                 'weird entry name': {'phrase': 'D\u00fcsseldorf'},
                       | 
                    
| ... | ... | 
                      @@ -1483,12 +1492,32 @@ contents go here  | 
                  
| 1483 | 1492 | 
                        ),  | 
                    
| 1484 | 1493 | 
                        id='service-weird-name-NFD',  | 
                    
| 1485 | 1494 | 
                        ),  | 
                    
| 1495 | 
                        + pytest.param(  | 
                    |
| 1496 | 
                        + textwrap.dedent(r"""  | 
                    |
| 1497 | 
                        + [vault.unicode-normalization-form]  | 
                    |
| 1498 | 
                        + 'weird entry name 2' = 'NFKD'  | 
                    |
| 1499 | 
                        + """),  | 
                    |
| 1500 | 
                        + ['--import', '-'],  | 
                    |
| 1501 | 
                        +                json.dumps({
                       | 
                    |
| 1502 | 
                        +                    'services': {
                       | 
                    |
| 1503 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1504 | 
                        +                        'weird entry name 1': {'phrase': 'D\u00fcsseldorf'},
                       | 
                    |
| 1505 | 
                        +                        'weird entry name 2': {'phrase': 'D\u00fcsseldorf'},
                       | 
                    |
| 1506 | 
                        + },  | 
                    |
| 1507 | 
                        + }),  | 
                    |
| 1508 | 
                        + (  | 
                    |
| 1509 | 
                        + 'the $.services["weird entry name 2"] passphrase '  | 
                    |
| 1510 | 
                        + 'is not NFKD-normalized'  | 
                    |
| 1511 | 
                        + ),  | 
                    |
| 1512 | 
                        + id='service-weird-name-2-NFKD',  | 
                    |
| 1513 | 
                        + ),  | 
                    |
| 1486 | 1514 | 
                        ],  | 
                    
| 1487 | 1515 | 
                        )  | 
                    
| 1488 | 1516 | 
                        def test_300_unicode_normalization_form_warning(  | 
                    
| 1489 | 1517 | 
                        self,  | 
                    
| 1490 | 1518 | 
                        monkeypatch: pytest.MonkeyPatch,  | 
                    
| 1491 | 1519 | 
                        caplog: pytest.LogCaptureFixture,  | 
                    
| 1520 | 
                        + main_config: str,  | 
                    |
| 1492 | 1521 | 
                        command_line: list[str],  | 
                    
| 1493 | 1522 | 
                        input: str | None,  | 
                    
| 1494 | 1523 | 
                        warning_message: str,  | 
                    
| ... | ... | 
                      @@ -1500,10 +1529,11 @@ contents go here  | 
                  
| 1500 | 1529 | 
                                     vault_config={
                       | 
                    
| 1501 | 1530 | 
                                         'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}
                       | 
                    
| 1502 | 1531 | 
                        },  | 
                    
| 1532 | 
                        + main_config_str=main_config,  | 
                    |
| 1503 | 1533 | 
                        ):  | 
                    
| 1504 | 1534 | 
                        _result = runner.invoke(  | 
                    
| 1505 | 1535 | 
                        cli.derivepassphrase_vault,  | 
                    
| 1506 | 
                        - command_line,  | 
                    |
| 1536 | 
                        + ['--debug', *command_line],  | 
                    |
| 1507 | 1537 | 
                        catch_exceptions=False,  | 
                    
| 1508 | 1538 | 
                        input=input,  | 
                    
| 1509 | 1539 | 
                        )  | 
                    
| ... | ... | 
                      @@ -1513,6 +1543,149 @@ contents go here  | 
                  
| 1513 | 1543 | 
                        warning_message, caplog.record_tuples  | 
                    
| 1514 | 1544 | 
                        ), 'expected known warning message in stderr'  | 
                    
| 1515 | 1545 | 
                         | 
                    
| 1546 | 
                        + @pytest.mark.parametrize(  | 
                    |
| 1547 | 
                        + ['main_config', 'command_line', 'input', 'error_message'],  | 
                    |
| 1548 | 
                        + [  | 
                    |
| 1549 | 
                        + pytest.param(  | 
                    |
| 1550 | 
                        + textwrap.dedent(r"""  | 
                    |
| 1551 | 
                        + [vault]  | 
                    |
| 1552 | 
                        + default-unicode-normalization-form = 'XXX'  | 
                    |
| 1553 | 
                        + """),  | 
                    |
| 1554 | 
                        + ['--import', '-'],  | 
                    |
| 1555 | 
                        +                json.dumps({
                       | 
                    |
| 1556 | 
                        +                    'services': {
                       | 
                    |
| 1557 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1558 | 
                        +                        'with_normalization': {'phrase': 'D\u00fcsseldorf'},
                       | 
                    |
| 1559 | 
                        + },  | 
                    |
| 1560 | 
                        + }),  | 
                    |
| 1561 | 
                        + (  | 
                    |
| 1562 | 
                        + "Invalid value 'XXX' for config key "  | 
                    |
| 1563 | 
                        + "vault.default-unicode-normalization-form"  | 
                    |
| 1564 | 
                        + ),  | 
                    |
| 1565 | 
                        + id='global',  | 
                    |
| 1566 | 
                        + ),  | 
                    |
| 1567 | 
                        + pytest.param(  | 
                    |
| 1568 | 
                        + textwrap.dedent(r"""  | 
                    |
| 1569 | 
                        + [vault.unicode-normalization-form]  | 
                    |
| 1570 | 
                        + with_normalization = 'XXX'  | 
                    |
| 1571 | 
                        + """),  | 
                    |
| 1572 | 
                        + ['--import', '-'],  | 
                    |
| 1573 | 
                        +                json.dumps({
                       | 
                    |
| 1574 | 
                        +                    'services': {
                       | 
                    |
| 1575 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 1576 | 
                        +                        'with_normalization': {'phrase': 'D\u00fcsseldorf'},
                       | 
                    |
| 1577 | 
                        + },  | 
                    |
| 1578 | 
                        + }),  | 
                    |
| 1579 | 
                        + (  | 
                    |
| 1580 | 
                        + "Invalid value 'XXX' for config key "  | 
                    |
| 1581 | 
                        + "vault.with_normalization.unicode-normalization-form"  | 
                    |
| 1582 | 
                        + ),  | 
                    |
| 1583 | 
                        + id='service',  | 
                    |
| 1584 | 
                        + ),  | 
                    |
| 1585 | 
                        + ],  | 
                    |
| 1586 | 
                        + )  | 
                    |
| 1587 | 
                        + def test_301_unicode_normalization_form_error(  | 
                    |
| 1588 | 
                        + self,  | 
                    |
| 1589 | 
                        + monkeypatch: pytest.MonkeyPatch,  | 
                    |
| 1590 | 
                        + main_config: str,  | 
                    |
| 1591 | 
                        + command_line: list[str],  | 
                    |
| 1592 | 
                        + input: str | None,  | 
                    |
| 1593 | 
                        + error_message: str,  | 
                    |
| 1594 | 
                        + ) -> None:  | 
                    |
| 1595 | 
                        + runner = click.testing.CliRunner(mix_stderr=False)  | 
                    |
| 1596 | 
                        + with tests.isolated_vault_config(  | 
                    |
| 1597 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 1598 | 
                        + runner=runner,  | 
                    |
| 1599 | 
                        +            vault_config={
                       | 
                    |
| 1600 | 
                        +                'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}
                       | 
                    |
| 1601 | 
                        + },  | 
                    |
| 1602 | 
                        + main_config_str=main_config,  | 
                    |
| 1603 | 
                        + ):  | 
                    |
| 1604 | 
                        + _result = runner.invoke(  | 
                    |
| 1605 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 1606 | 
                        + command_line,  | 
                    |
| 1607 | 
                        + catch_exceptions=False,  | 
                    |
| 1608 | 
                        + input=input,  | 
                    |
| 1609 | 
                        + )  | 
                    |
| 1610 | 
                        + result = tests.ReadableResult.parse(_result)  | 
                    |
| 1611 | 
                        + assert result.error_exit(  | 
                    |
| 1612 | 
                        + error='The configuration file is invalid.'  | 
                    |
| 1613 | 
                        + ), 'expected error exit and known error message'  | 
                    |
| 1614 | 
                        + assert result.error_exit(  | 
                    |
| 1615 | 
                        + error=error_message  | 
                    |
| 1616 | 
                        + ), 'expected error exit and known error message'  | 
                    |
| 1617 | 
                        +  | 
                    |
| 1618 | 
                        + @pytest.mark.parametrize(  | 
                    |
| 1619 | 
                        + 'command_line',  | 
                    |
| 1620 | 
                        + [  | 
                    |
| 1621 | 
                        + pytest.param(  | 
                    |
| 1622 | 
                        + ['--config', '--phrase'],  | 
                    |
| 1623 | 
                        + id='configure global passphrase',  | 
                    |
| 1624 | 
                        + ),  | 
                    |
| 1625 | 
                        + pytest.param(  | 
                    |
| 1626 | 
                        + ['--phrase', '--', DUMMY_SERVICE],  | 
                    |
| 1627 | 
                        + id='interactive passphrase',  | 
                    |
| 1628 | 
                        + ),  | 
                    |
| 1629 | 
                        + ],  | 
                    |
| 1630 | 
                        + )  | 
                    |
| 1631 | 
                        + def test_301a_unicode_normalization_form_error_from_stored_config(  | 
                    |
| 1632 | 
                        + self,  | 
                    |
| 1633 | 
                        + monkeypatch: pytest.MonkeyPatch,  | 
                    |
| 1634 | 
                        + command_line: list[str],  | 
                    |
| 1635 | 
                        + ) -> None:  | 
                    |
| 1636 | 
                        + runner = click.testing.CliRunner(mix_stderr=False)  | 
                    |
| 1637 | 
                        + with tests.isolated_vault_config(  | 
                    |
| 1638 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 1639 | 
                        + runner=runner,  | 
                    |
| 1640 | 
                        +            vault_config={
                       | 
                    |
| 1641 | 
                        +                'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}
                       | 
                    |
| 1642 | 
                        + },  | 
                    |
| 1643 | 
                        +            main_config_str=textwrap.dedent("""
                       | 
                    |
| 1644 | 
                        + [vault]  | 
                    |
| 1645 | 
                        + default-unicode-normalization-form = 'XXX'  | 
                    |
| 1646 | 
                        + """),  | 
                    |
| 1647 | 
                        + ):  | 
                    |
| 1648 | 
                        + _result = runner.invoke(  | 
                    |
| 1649 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 1650 | 
                        + command_line,  | 
                    |
| 1651 | 
                        + input=DUMMY_PASSPHRASE,  | 
                    |
| 1652 | 
                        + catch_exceptions=False,  | 
                    |
| 1653 | 
                        + )  | 
                    |
| 1654 | 
                        + result = tests.ReadableResult.parse(_result)  | 
                    |
| 1655 | 
                        + assert result.error_exit(  | 
                    |
| 1656 | 
                        + error='The configuration file is invalid.'  | 
                    |
| 1657 | 
                        + ), 'expected error exit and known error message'  | 
                    |
| 1658 | 
                        + assert result.error_exit(  | 
                    |
| 1659 | 
                        + error=(  | 
                    |
| 1660 | 
                        + "Invalid value 'XXX' for config key "  | 
                    |
| 1661 | 
                        + "vault.default-unicode-normalization-form"  | 
                    |
| 1662 | 
                        + ),  | 
                    |
| 1663 | 
                        + ), 'expected error exit and known error message'  | 
                    |
| 1664 | 
                        +  | 
                    |
| 1665 | 
                        + def test_310_bad_user_config_file(  | 
                    |
| 1666 | 
                        + self,  | 
                    |
| 1667 | 
                        + monkeypatch: pytest.MonkeyPatch,  | 
                    |
| 1668 | 
                        + ) -> None:  | 
                    |
| 1669 | 
                        + runner = click.testing.CliRunner(mix_stderr=False)  | 
                    |
| 1670 | 
                        + with tests.isolated_vault_config(  | 
                    |
| 1671 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 1672 | 
                        + runner=runner,  | 
                    |
| 1673 | 
                        +            vault_config={'services': {}},
                       | 
                    |
| 1674 | 
                        +            main_config_str=textwrap.dedent("""
                       | 
                    |
| 1675 | 
                        + This file is not valid TOML.  | 
                    |
| 1676 | 
                        + """),  | 
                    |
| 1677 | 
                        + ):  | 
                    |
| 1678 | 
                        + _result = runner.invoke(  | 
                    |
| 1679 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 1680 | 
                        + ['--phrase', '--', DUMMY_SERVICE],  | 
                    |
| 1681 | 
                        + input=DUMMY_PASSPHRASE,  | 
                    |
| 1682 | 
                        + catch_exceptions=False,  | 
                    |
| 1683 | 
                        + )  | 
                    |
| 1684 | 
                        + result = tests.ReadableResult.parse(_result)  | 
                    |
| 1685 | 
                        + assert result.error_exit(  | 
                    |
| 1686 | 
                        + error='Cannot load user config:'  | 
                    |
| 1687 | 
                        + ), 'expected error exit and known error message'  | 
                    |
| 1688 | 
                        +  | 
                    |
| 1516 | 1689 | 
                        def test_400_missing_af_unix_support(  | 
                    
| 1517 | 1690 | 
                        self,  | 
                    
| 1518 | 1691 | 
                        monkeypatch: pytest.MonkeyPatch,  | 
                    
| 1519 | 1692 |