Consolidate test parametrizations per test module
Marco Ricci

Marco Ricci commited on 2025-02-01 15:55:49
Zeige 8 geänderte Dateien mit 1807 Einfügungen und 1778 Löschungen.


Collect all calls to `pytest.mark.parametrize` in a per-module enum.
This deduplicates the definitions and highlights inconsistencies.

(For the CLI test module, the shell formatter functions were moved above
the enum to avoid NameErrors.)
... ...
@@ -7,6 +7,7 @@ from __future__ import annotations
7 7
 import base64
8 8
 import contextlib
9 9
 import copy
10
+import enum
10 11
 import errno
11 12
 import io
12 13
 import json
... ...
@@ -294,6 +295,1019 @@ def vault_config_exporter_shell_interpreter(  # noqa: C901
294 295
         )
295 296
 
296 297
 
298
+def bash_format(item: click.shell_completion.CompletionItem) -> str:
299
+    """A formatter for `bash`-style shell completion items.
300
+
301
+    The format is `type,value`, and is dictated by [`click`][].
302
+
303
+    """
304
+    type, value = (  # noqa: A001
305
+        item.type,
306
+        item.value,
307
+    )
308
+    return f'{type},{value}'
309
+
310
+
311
+def fish_format(item: click.shell_completion.CompletionItem) -> str:
312
+    r"""A formatter for `fish`-style shell completion items.
313
+
314
+    The format is `type,value<tab>help`, and is dictated by [`click`][].
315
+
316
+    """
317
+    type, value, help = (  # noqa: A001
318
+        item.type,
319
+        item.value,
320
+        item.help,
321
+    )
322
+    return f'{type},{value}\t{help}' if help else f'{type},{value}'
323
+
324
+
325
+def zsh_format(item: click.shell_completion.CompletionItem) -> str:
326
+    r"""A formatter for `zsh`-style shell completion items.
327
+
328
+    The format is `type<newline>value<newline>help<newline>`, and is
329
+    dictated by [`click`][].  Upstream `click` currently (v8.2.0) does
330
+    not deal with colons in the value correctly when the help text is
331
+    non-degenerate.  Our formatter here does, provided the upstream
332
+    `zsh` completion script is used; see the
333
+    [`cli_machinery.ZshComplete`][] class.  A request is underway to
334
+    merge this change into upstream `click`; see
335
+    [`pallets/click#2846`][PR2846].
336
+
337
+    [PR2846]: https://github.com/pallets/click/pull/2846
338
+
339
+    """
340
+    empty_help = '_'
341
+    help_, value = (
342
+        (item.help, item.value.replace(':', r'\:'))
343
+        if item.help and item.help == empty_help
344
+        else (empty_help, item.value)
345
+    )
346
+    return f'{item.type}\n{value}\n{help_}'
347
+
348
+
349
+class Parametrizations(enum.Enum):
350
+    EAGER_ARGUMENTS = pytest.mark.parametrize(
351
+        'arguments',
352
+        [['--help'], ['--version']],
353
+        ids=['help', 'version'],
354
+    )
355
+    CHARSET_NAME = pytest.mark.parametrize(
356
+        'charset_name', ['lower', 'upper', 'number', 'space', 'dash', 'symbol']
357
+    )
358
+    COMMAND_NON_EAGER_ARGUMENTS = pytest.mark.parametrize(
359
+        ['command', 'non_eager_arguments'],
360
+        [
361
+            pytest.param(
362
+                [],
363
+                [],
364
+                id='top-nothing',
365
+            ),
366
+            pytest.param(
367
+                [],
368
+                ['export'],
369
+                id='top-export',
370
+            ),
371
+            pytest.param(
372
+                ['export'],
373
+                [],
374
+                id='export-nothing',
375
+            ),
376
+            pytest.param(
377
+                ['export'],
378
+                ['vault'],
379
+                id='export-vault',
380
+            ),
381
+            pytest.param(
382
+                ['export', 'vault'],
383
+                [],
384
+                id='export-vault-nothing',
385
+            ),
386
+            pytest.param(
387
+                ['export', 'vault'],
388
+                ['--format', 'this-format-doesnt-exist'],
389
+                id='export-vault-args',
390
+            ),
391
+            pytest.param(
392
+                ['vault'],
393
+                [],
394
+                id='vault-nothing',
395
+            ),
396
+            pytest.param(
397
+                ['vault'],
398
+                ['--export', './'],
399
+                id='vault-args',
400
+            ),
401
+        ],
402
+    )
403
+    # TODO(the-13th-letter): Add "configure service passphrase" example.
404
+    UNICODE_NORMALIZATION_COMMAND_LINES = pytest.mark.parametrize(
405
+        'command_line',
406
+        [
407
+            pytest.param(
408
+                ['--config', '--phrase'],
409
+                id='configure global passphrase',
410
+            ),
411
+            pytest.param(
412
+                ['--phrase', '--', DUMMY_SERVICE],
413
+                id='interactive passphrase',
414
+            ),
415
+        ],
416
+    )
417
+    DELETE_CONFIG_INPUT = pytest.mark.parametrize(
418
+        ['command_line', 'config', 'result_config'],
419
+        [
420
+            pytest.param(
421
+                ['--delete-globals'],
422
+                {'global': {'phrase': 'abc'}, 'services': {}},
423
+                {'services': {}},
424
+                id='globals',
425
+            ),
426
+            pytest.param(
427
+                ['--delete', '--', DUMMY_SERVICE],
428
+                {
429
+                    'global': {'phrase': 'abc'},
430
+                    'services': {DUMMY_SERVICE: {'notes': '...'}},
431
+                },
432
+                {'global': {'phrase': 'abc'}, 'services': {}},
433
+                id='service',
434
+            ),
435
+            pytest.param(
436
+                ['--clear'],
437
+                {
438
+                    'global': {'phrase': 'abc'},
439
+                    'services': {DUMMY_SERVICE: {'notes': '...'}},
440
+                },
441
+                {'services': {}},
442
+                id='all',
443
+            ),
444
+        ],
445
+    )
446
+    COLORFUL_COMMAND_INPUT = pytest.mark.parametrize(
447
+        ['command_line', 'input'],
448
+        [
449
+            (
450
+                ['vault', '--import', '-'],
451
+                '{"services": {"": {"length": 20}}}',
452
+            ),
453
+        ],
454
+        ids=['cmd'],
455
+    )
456
+    CONFIG_EDITING_VIA_CONFIG_FLAG_FAILURES = pytest.mark.parametrize(
457
+        ['command_line', 'input', 'err_text'],
458
+        [
459
+            pytest.param(
460
+                [],
461
+                '',
462
+                'Cannot update the global settings without any given settings',
463
+                id='None',
464
+            ),
465
+            pytest.param(
466
+                ['--', 'sv'],
467
+                '',
468
+                'Cannot update the service-specific settings without any given settings',
469
+                id='None-sv',
470
+            ),
471
+            pytest.param(
472
+                ['--phrase', '--', 'sv'],
473
+                '',
474
+                'No passphrase was given',
475
+                id='phrase-sv',
476
+            ),
477
+            pytest.param(
478
+                ['--key'],
479
+                '',
480
+                'No SSH key was selected',
481
+                id='key-sv',
482
+            ),
483
+        ],
484
+    )
485
+    CONFIG_EDITING_VIA_CONFIG_FLAG = pytest.mark.parametrize(
486
+        ['command_line', 'input', 'result_config'],
487
+        [
488
+            pytest.param(
489
+                ['--phrase'],
490
+                'my passphrase\n',
491
+                {'global': {'phrase': 'my passphrase'}, 'services': {}},
492
+                id='phrase',
493
+            ),
494
+            pytest.param(
495
+                ['--key'],
496
+                '1\n',
497
+                {
498
+                    'global': {'key': DUMMY_KEY1_B64, 'phrase': 'abc'},
499
+                    'services': {},
500
+                },
501
+                id='key',
502
+            ),
503
+            pytest.param(
504
+                ['--phrase', '--', 'sv'],
505
+                'my passphrase\n',
506
+                {
507
+                    'global': {'phrase': 'abc'},
508
+                    'services': {'sv': {'phrase': 'my passphrase'}},
509
+                },
510
+                id='phrase-sv',
511
+            ),
512
+            pytest.param(
513
+                ['--key', '--', 'sv'],
514
+                '1\n',
515
+                {
516
+                    'global': {'phrase': 'abc'},
517
+                    'services': {'sv': {'key': DUMMY_KEY1_B64}},
518
+                },
519
+                id='key-sv',
520
+            ),
521
+            pytest.param(
522
+                ['--key', '--length', '15', '--', 'sv'],
523
+                '1\n',
524
+                {
525
+                    'global': {'phrase': 'abc'},
526
+                    'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}},
527
+                },
528
+                id='key-length-sv',
529
+            ),
530
+        ],
531
+    )
532
+    COMPLETABLE_PATH_ARGUMENT = pytest.mark.parametrize(
533
+        'command_prefix',
534
+        [
535
+            pytest.param(
536
+                ('export', 'vault'),
537
+                id='derivepassphrase-export-vault',
538
+            ),
539
+            pytest.param(
540
+                ('vault', '--export'),
541
+                id='derivepassphrase-vault--export',
542
+            ),
543
+            pytest.param(
544
+                ('vault', '--import'),
545
+                id='derivepassphrase-vault--import',
546
+            ),
547
+        ],
548
+    )
549
+    COMPLETABLE_OPTIONS = pytest.mark.parametrize(
550
+        ['command_prefix', 'incomplete', 'completions'],
551
+        [
552
+            pytest.param(
553
+                (),
554
+                '-',
555
+                frozenset({
556
+                    '--help',
557
+                    '-h',
558
+                    '--version',
559
+                    '--debug',
560
+                    '--verbose',
561
+                    '-v',
562
+                    '--quiet',
563
+                    '-q',
564
+                }),
565
+                id='derivepassphrase',
566
+            ),
567
+            pytest.param(
568
+                ('export',),
569
+                '-',
570
+                frozenset({
571
+                    '--help',
572
+                    '-h',
573
+                    '--version',
574
+                    '--debug',
575
+                    '--verbose',
576
+                    '-v',
577
+                    '--quiet',
578
+                    '-q',
579
+                }),
580
+                id='derivepassphrase-export',
581
+            ),
582
+            pytest.param(
583
+                ('export', 'vault'),
584
+                '-',
585
+                frozenset({
586
+                    '--help',
587
+                    '-h',
588
+                    '--version',
589
+                    '--debug',
590
+                    '--verbose',
591
+                    '-v',
592
+                    '--quiet',
593
+                    '-q',
594
+                    '--format',
595
+                    '-f',
596
+                    '--key',
597
+                    '-k',
598
+                }),
599
+                id='derivepassphrase-export-vault',
600
+            ),
601
+            pytest.param(
602
+                ('vault',),
603
+                '-',
604
+                frozenset({
605
+                    '--help',
606
+                    '-h',
607
+                    '--version',
608
+                    '--debug',
609
+                    '--verbose',
610
+                    '-v',
611
+                    '--quiet',
612
+                    '-q',
613
+                    '--phrase',
614
+                    '-p',
615
+                    '--key',
616
+                    '-k',
617
+                    '--length',
618
+                    '-l',
619
+                    '--repeat',
620
+                    '-r',
621
+                    '--upper',
622
+                    '--lower',
623
+                    '--number',
624
+                    '--space',
625
+                    '--dash',
626
+                    '--symbol',
627
+                    '--config',
628
+                    '-c',
629
+                    '--notes',
630
+                    '-n',
631
+                    '--delete',
632
+                    '-x',
633
+                    '--delete-globals',
634
+                    '--clear',
635
+                    '-X',
636
+                    '--export',
637
+                    '-e',
638
+                    '--import',
639
+                    '-i',
640
+                    '--overwrite-existing',
641
+                    '--merge-existing',
642
+                    '--unset',
643
+                    '--export-as',
644
+                }),
645
+                id='derivepassphrase-vault',
646
+            ),
647
+        ],
648
+    )
649
+    COMPLETABLE_SUBCOMMANDS = pytest.mark.parametrize(
650
+        ['command_prefix', 'incomplete', 'completions'],
651
+        [
652
+            pytest.param(
653
+                (),
654
+                '',
655
+                frozenset({'export', 'vault'}),
656
+                id='derivepassphrase',
657
+            ),
658
+            pytest.param(
659
+                ('export',),
660
+                '',
661
+                frozenset({'vault'}),
662
+                id='derivepassphrase-export',
663
+            ),
664
+        ],
665
+    )
666
+    BAD_CONFIGS = pytest.mark.parametrize(
667
+        'config',
668
+        [
669
+            {'global': '', 'services': {}},
670
+            {'global': 0, 'services': {}},
671
+            {
672
+                'global': {'phrase': 'abc'},
673
+                'services': False,
674
+            },
675
+            {
676
+                'global': {'phrase': 'abc'},
677
+                'services': True,
678
+            },
679
+            {
680
+                'global': {'phrase': 'abc'},
681
+                'services': None,
682
+            },
683
+        ],
684
+    )
685
+    BASE_CONFIG_VARIATIONS = pytest.mark.parametrize(
686
+        'config',
687
+        [
688
+            {'global': {'phrase': 'my passphrase'}, 'services': {}},
689
+            {'global': {'key': DUMMY_KEY1_B64}, 'services': {}},
690
+            {
691
+                'global': {'phrase': 'abc'},
692
+                'services': {'sv': {'phrase': 'my passphrase'}},
693
+            },
694
+            {
695
+                'global': {'phrase': 'abc'},
696
+                'services': {'sv': {'key': DUMMY_KEY1_B64}},
697
+            },
698
+            {
699
+                'global': {'phrase': 'abc'},
700
+                'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}},
701
+            },
702
+        ],
703
+    )
704
+    BASE_CONFIG_WITH_KEY_VARIATIONS = pytest.mark.parametrize(
705
+        'config',
706
+        [
707
+            pytest.param(
708
+                {
709
+                    'global': {'key': DUMMY_KEY1_B64},
710
+                    'services': {DUMMY_SERVICE: {}},
711
+                },
712
+                id='global_config',
713
+            ),
714
+            pytest.param(
715
+                {'services': {DUMMY_SERVICE: {'key': DUMMY_KEY2_B64}}},
716
+                id='service_config',
717
+            ),
718
+            pytest.param(
719
+                {
720
+                    'global': {'key': DUMMY_KEY1_B64},
721
+                    'services': {DUMMY_SERVICE: {'key': DUMMY_KEY2_B64}},
722
+                },
723
+                id='full_config',
724
+            ),
725
+        ],
726
+    )
727
+    CONFIG_WITH_KEY = pytest.mark.parametrize(
728
+        'config',
729
+        [
730
+            pytest.param(
731
+                {
732
+                    'global': {'key': DUMMY_KEY1_B64},
733
+                    'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS},
734
+                },
735
+                id='global',
736
+            ),
737
+            pytest.param(
738
+                {
739
+                    'global': {'phrase': DUMMY_PASSPHRASE.rstrip('\n')},
740
+                    'services': {
741
+                        DUMMY_SERVICE: {
742
+                            'key': DUMMY_KEY1_B64,
743
+                            **DUMMY_CONFIG_SETTINGS,
744
+                        }
745
+                    },
746
+                },
747
+                id='service',
748
+            ),
749
+        ],
750
+    )
751
+    VALID_TEST_CONFIGS = pytest.mark.parametrize(
752
+        'config',
753
+        [
754
+            conf.config
755
+            for conf in TEST_CONFIGS
756
+            if tests.is_valid_test_config(conf)
757
+        ],
758
+    )
759
+    KEY_OVERRIDING_IN_CONFIG = pytest.mark.parametrize(
760
+        ['config', 'command_line'],
761
+        [
762
+            pytest.param(
763
+                {
764
+                    'global': {'key': DUMMY_KEY1_B64},
765
+                    'services': {},
766
+                },
767
+                ['--config', '-p'],
768
+                id='global',
769
+            ),
770
+            pytest.param(
771
+                {
772
+                    'services': {
773
+                        DUMMY_SERVICE: {
774
+                            'key': DUMMY_KEY1_B64,
775
+                            **DUMMY_CONFIG_SETTINGS,
776
+                        },
777
+                    },
778
+                },
779
+                ['--config', '-p', '--', DUMMY_SERVICE],
780
+                id='service',
781
+            ),
782
+            pytest.param(
783
+                {
784
+                    'global': {'key': DUMMY_KEY1_B64},
785
+                    'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()},
786
+                },
787
+                ['--config', '-p', '--', DUMMY_SERVICE],
788
+                id='service-over-global',
789
+            ),
790
+        ],
791
+    )
792
+    COMPLETION_FUNCTION_INPUTS = pytest.mark.parametrize(
793
+        ['config', 'comp_func', 'args', 'incomplete', 'results'],
794
+        [
795
+            pytest.param(
796
+                {'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}},
797
+                cli_helpers.shell_complete_service,
798
+                ['vault'],
799
+                '',
800
+                [DUMMY_SERVICE],
801
+                id='base_config-service',
802
+            ),
803
+            pytest.param(
804
+                {'services': {}},
805
+                cli_helpers.shell_complete_service,
806
+                ['vault'],
807
+                '',
808
+                [],
809
+                id='empty_config-service',
810
+            ),
811
+            pytest.param(
812
+                {
813
+                    'services': {
814
+                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
815
+                        'newline\nin\nname': DUMMY_CONFIG_SETTINGS.copy(),
816
+                    }
817
+                },
818
+                cli_helpers.shell_complete_service,
819
+                ['vault'],
820
+                '',
821
+                [DUMMY_SERVICE],
822
+                id='incompletable_newline_config-service',
823
+            ),
824
+            pytest.param(
825
+                {
826
+                    'services': {
827
+                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
828
+                        'backspace\bin\bname': DUMMY_CONFIG_SETTINGS.copy(),
829
+                    }
830
+                },
831
+                cli_helpers.shell_complete_service,
832
+                ['vault'],
833
+                '',
834
+                [DUMMY_SERVICE],
835
+                id='incompletable_backspace_config-service',
836
+            ),
837
+            pytest.param(
838
+                {
839
+                    'services': {
840
+                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
841
+                        'colon:in:name': DUMMY_CONFIG_SETTINGS.copy(),
842
+                    }
843
+                },
844
+                cli_helpers.shell_complete_service,
845
+                ['vault'],
846
+                '',
847
+                sorted([DUMMY_SERVICE, 'colon:in:name']),
848
+                id='brittle_colon_config-service',
849
+            ),
850
+            pytest.param(
851
+                {
852
+                    'services': {
853
+                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
854
+                        'colon:in:name': DUMMY_CONFIG_SETTINGS.copy(),
855
+                        'newline\nin\nname': DUMMY_CONFIG_SETTINGS.copy(),
856
+                        'backspace\bin\bname': DUMMY_CONFIG_SETTINGS.copy(),
857
+                        'nul\x00in\x00name': DUMMY_CONFIG_SETTINGS.copy(),
858
+                        'del\x7fin\x7fname': DUMMY_CONFIG_SETTINGS.copy(),
859
+                    }
860
+                },
861
+                cli_helpers.shell_complete_service,
862
+                ['vault'],
863
+                '',
864
+                sorted([DUMMY_SERVICE, 'colon:in:name']),
865
+                id='brittle_incompletable_multi_config-service',
866
+            ),
867
+            pytest.param(
868
+                {'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}},
869
+                cli_helpers.shell_complete_path,
870
+                ['vault', '--import'],
871
+                '',
872
+                [click.shell_completion.CompletionItem('', type='file')],
873
+                id='base_config-path',
874
+            ),
875
+            pytest.param(
876
+                {'services': {}},
877
+                cli_helpers.shell_complete_path,
878
+                ['vault', '--import'],
879
+                '',
880
+                [click.shell_completion.CompletionItem('', type='file')],
881
+                id='empty_config-path',
882
+            ),
883
+        ],
884
+    )
885
+    COMPLETABLE_SERVICE_NAMES = pytest.mark.parametrize(
886
+        ['config', 'incomplete', 'completions'],
887
+        [
888
+            pytest.param(
889
+                {'services': {}},
890
+                '',
891
+                frozenset(),
892
+                id='no_services',
893
+            ),
894
+            pytest.param(
895
+                {'services': {}},
896
+                'partial',
897
+                frozenset(),
898
+                id='no_services_partial',
899
+            ),
900
+            pytest.param(
901
+                {'services': {DUMMY_SERVICE: {'length': 10}}},
902
+                '',
903
+                frozenset({DUMMY_SERVICE}),
904
+                id='one_service',
905
+            ),
906
+            pytest.param(
907
+                {'services': {DUMMY_SERVICE: {'length': 10}}},
908
+                DUMMY_SERVICE[:4],
909
+                frozenset({DUMMY_SERVICE}),
910
+                id='one_service_partial',
911
+            ),
912
+            pytest.param(
913
+                {'services': {DUMMY_SERVICE: {'length': 10}}},
914
+                DUMMY_SERVICE[-4:],
915
+                frozenset(),
916
+                id='one_service_partial_miss',
917
+            ),
918
+        ],
919
+    )
920
+    SERVICE_NAME_COMPLETION_INPUTS = pytest.mark.parametrize(
921
+        ['config', 'key', 'incomplete', 'completions'],
922
+        [
923
+            pytest.param(
924
+                {
925
+                    'services': {
926
+                        DUMMY_SERVICE: {'length': 10},
927
+                        'newline\nin\nname': {'length': 10},
928
+                    },
929
+                },
930
+                'newline\nin\nname',
931
+                '',
932
+                frozenset({DUMMY_SERVICE}),
933
+                id='newline',
934
+            ),
935
+            pytest.param(
936
+                {
937
+                    'services': {
938
+                        DUMMY_SERVICE: {'length': 10},
939
+                        'newline\nin\nname': {'length': 10},
940
+                    },
941
+                },
942
+                'newline\nin\nname',
943
+                'serv',
944
+                frozenset({DUMMY_SERVICE}),
945
+                id='newline_partial_other',
946
+            ),
947
+            pytest.param(
948
+                {
949
+                    'services': {
950
+                        DUMMY_SERVICE: {'length': 10},
951
+                        'newline\nin\nname': {'length': 10},
952
+                    },
953
+                },
954
+                'newline\nin\nname',
955
+                'newline',
956
+                frozenset({}),
957
+                id='newline_partial_specific',
958
+            ),
959
+            pytest.param(
960
+                {
961
+                    'services': {
962
+                        DUMMY_SERVICE: {'length': 10},
963
+                        'nul\x00in\x00name': {'length': 10},
964
+                    },
965
+                },
966
+                'nul\x00in\x00name',
967
+                '',
968
+                frozenset({DUMMY_SERVICE}),
969
+                id='nul',
970
+            ),
971
+            pytest.param(
972
+                {
973
+                    'services': {
974
+                        DUMMY_SERVICE: {'length': 10},
975
+                        'nul\x00in\x00name': {'length': 10},
976
+                    },
977
+                },
978
+                'nul\x00in\x00name',
979
+                'serv',
980
+                frozenset({DUMMY_SERVICE}),
981
+                id='nul_partial_other',
982
+            ),
983
+            pytest.param(
984
+                {
985
+                    'services': {
986
+                        DUMMY_SERVICE: {'length': 10},
987
+                        'nul\x00in\x00name': {'length': 10},
988
+                    },
989
+                },
990
+                'nul\x00in\x00name',
991
+                'nul',
992
+                frozenset({}),
993
+                id='nul_partial_specific',
994
+            ),
995
+            pytest.param(
996
+                {
997
+                    'services': {
998
+                        DUMMY_SERVICE: {'length': 10},
999
+                        'backspace\bin\bname': {'length': 10},
1000
+                    },
1001
+                },
1002
+                'backspace\bin\bname',
1003
+                '',
1004
+                frozenset({DUMMY_SERVICE}),
1005
+                id='backspace',
1006
+            ),
1007
+            pytest.param(
1008
+                {
1009
+                    'services': {
1010
+                        DUMMY_SERVICE: {'length': 10},
1011
+                        'backspace\bin\bname': {'length': 10},
1012
+                    },
1013
+                },
1014
+                'backspace\bin\bname',
1015
+                'serv',
1016
+                frozenset({DUMMY_SERVICE}),
1017
+                id='backspace_partial_other',
1018
+            ),
1019
+            pytest.param(
1020
+                {
1021
+                    'services': {
1022
+                        DUMMY_SERVICE: {'length': 10},
1023
+                        'backspace\bin\bname': {'length': 10},
1024
+                    },
1025
+                },
1026
+                'backspace\bin\bname',
1027
+                'back',
1028
+                frozenset({}),
1029
+                id='backspace_partial_specific',
1030
+            ),
1031
+            pytest.param(
1032
+                {
1033
+                    'services': {
1034
+                        DUMMY_SERVICE: {'length': 10},
1035
+                        'del\x7fin\x7fname': {'length': 10},
1036
+                    },
1037
+                },
1038
+                'del\x7fin\x7fname',
1039
+                '',
1040
+                frozenset({DUMMY_SERVICE}),
1041
+                id='del',
1042
+            ),
1043
+            pytest.param(
1044
+                {
1045
+                    'services': {
1046
+                        DUMMY_SERVICE: {'length': 10},
1047
+                        'del\x7fin\x7fname': {'length': 10},
1048
+                    },
1049
+                },
1050
+                'del\x7fin\x7fname',
1051
+                'serv',
1052
+                frozenset({DUMMY_SERVICE}),
1053
+                id='del_partial_other',
1054
+            ),
1055
+            pytest.param(
1056
+                {
1057
+                    'services': {
1058
+                        DUMMY_SERVICE: {'length': 10},
1059
+                        'del\x7fin\x7fname': {'length': 10},
1060
+                    },
1061
+                },
1062
+                'del\x7fin\x7fname',
1063
+                'del',
1064
+                frozenset({}),
1065
+                id='del_partial_specific',
1066
+            ),
1067
+        ],
1068
+    )
1069
+    CONNECTION_HINTS = pytest.mark.parametrize(
1070
+        'conn_hint', ['none', 'socket', 'client']
1071
+    )
1072
+    SERVICE_NAME_EXCEPTIONS = pytest.mark.parametrize(
1073
+        'exc_type', [RuntimeError, KeyError, ValueError]
1074
+    )
1075
+    EXPORT_FORMAT_OPTIONS = pytest.mark.parametrize(
1076
+        'export_options',
1077
+        [
1078
+            [],
1079
+            ['--export-as=sh'],
1080
+        ],
1081
+    )
1082
+    FORCE_COLOR = pytest.mark.parametrize(
1083
+        'force_color',
1084
+        [False, True],
1085
+        ids=['noforce', 'force'],
1086
+    )
1087
+    INCOMPLETE = pytest.mark.parametrize('incomplete', ['', 'partial'])
1088
+    ISATTY = pytest.mark.parametrize(
1089
+        'isatty',
1090
+        [False, True],
1091
+        ids=['notty', 'tty'],
1092
+    )
1093
+    KEY_INDEX = pytest.mark.parametrize(
1094
+        'key_index', [1, 2, 3], ids=lambda i: f'index{i}'
1095
+    )
1096
+    UNICODE_NORMALIZATION_ERROR_INPUTS = pytest.mark.parametrize(
1097
+        ['main_config', 'command_line', 'input', 'error_message'],
1098
+        [
1099
+            pytest.param(
1100
+                textwrap.dedent(r"""
1101
+                [vault]
1102
+                default-unicode-normalization-form = 'XXX'
1103
+                """),
1104
+                ['--import', '-'],
1105
+                json.dumps({
1106
+                    'services': {
1107
+                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1108
+                        'with_normalization': {'phrase': 'D\u00fcsseldorf'},
1109
+                    },
1110
+                }),
1111
+                (
1112
+                    "Invalid value 'XXX' for config key "
1113
+                    'vault.default-unicode-normalization-form'
1114
+                ),
1115
+                id='global',
1116
+            ),
1117
+            pytest.param(
1118
+                textwrap.dedent(r"""
1119
+                [vault.unicode-normalization-form]
1120
+                with_normalization = 'XXX'
1121
+                """),
1122
+                ['--import', '-'],
1123
+                json.dumps({
1124
+                    'services': {
1125
+                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1126
+                        'with_normalization': {'phrase': 'D\u00fcsseldorf'},
1127
+                    },
1128
+                }),
1129
+                (
1130
+                    "Invalid value 'XXX' for config key "
1131
+                    'vault.with_normalization.unicode-normalization-form'
1132
+                ),
1133
+                id='service',
1134
+            ),
1135
+        ],
1136
+    )
1137
+    UNICODE_NORMALIZATION_WARNING_INPUTS = pytest.mark.parametrize(
1138
+        ['main_config', 'command_line', 'input', 'warning_message'],
1139
+        [
1140
+            pytest.param(
1141
+                '',
1142
+                ['--import', '-'],
1143
+                json.dumps({
1144
+                    'global': {'phrase': 'Du\u0308sseldorf'},
1145
+                    'services': {},
1146
+                }),
1147
+                'The $.global passphrase is not NFC-normalized',
1148
+                id='global-NFC',
1149
+            ),
1150
+            pytest.param(
1151
+                '',
1152
+                ['--import', '-'],
1153
+                json.dumps({
1154
+                    'services': {
1155
+                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1156
+                        'weird entry name': {'phrase': 'Du\u0308sseldorf'},
1157
+                    }
1158
+                }),
1159
+                (
1160
+                    'The $.services["weird entry name"] passphrase '
1161
+                    'is not NFC-normalized'
1162
+                ),
1163
+                id='service-weird-name-NFC',
1164
+            ),
1165
+            pytest.param(
1166
+                '',
1167
+                ['--config', '-p', '--', DUMMY_SERVICE],
1168
+                'Du\u0308sseldorf',
1169
+                (
1170
+                    f'The $.services.{DUMMY_SERVICE} passphrase '
1171
+                    f'is not NFC-normalized'
1172
+                ),
1173
+                id='config-NFC',
1174
+            ),
1175
+            pytest.param(
1176
+                '',
1177
+                ['-p', '--', DUMMY_SERVICE],
1178
+                'Du\u0308sseldorf',
1179
+                'The interactive input passphrase is not NFC-normalized',
1180
+                id='direct-input-NFC',
1181
+            ),
1182
+            pytest.param(
1183
+                textwrap.dedent(r"""
1184
+                [vault]
1185
+                default-unicode-normalization-form = 'NFD'
1186
+                """),
1187
+                ['--import', '-'],
1188
+                json.dumps({
1189
+                    'global': {
1190
+                        'phrase': 'D\u00fcsseldorf',
1191
+                    },
1192
+                    'services': {},
1193
+                }),
1194
+                'The $.global passphrase is not NFD-normalized',
1195
+                id='global-NFD',
1196
+            ),
1197
+            pytest.param(
1198
+                textwrap.dedent(r"""
1199
+                [vault]
1200
+                default-unicode-normalization-form = 'NFD'
1201
+                """),
1202
+                ['--import', '-'],
1203
+                json.dumps({
1204
+                    'services': {
1205
+                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1206
+                        'weird entry name': {'phrase': 'D\u00fcsseldorf'},
1207
+                    },
1208
+                }),
1209
+                (
1210
+                    'The $.services["weird entry name"] passphrase '
1211
+                    'is not NFD-normalized'
1212
+                ),
1213
+                id='service-weird-name-NFD',
1214
+            ),
1215
+            pytest.param(
1216
+                textwrap.dedent(r"""
1217
+                [vault.unicode-normalization-form]
1218
+                'weird entry name 2' = 'NFKD'
1219
+                """),
1220
+                ['--import', '-'],
1221
+                json.dumps({
1222
+                    'services': {
1223
+                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1224
+                        'weird entry name 1': {'phrase': 'D\u00fcsseldorf'},
1225
+                        'weird entry name 2': {'phrase': 'D\u00fcsseldorf'},
1226
+                    },
1227
+                }),
1228
+                (
1229
+                    'The $.services["weird entry name 2"] passphrase '
1230
+                    'is not NFKD-normalized'
1231
+                ),
1232
+                id='service-weird-name-2-NFKD',
1233
+            ),
1234
+        ],
1235
+    )
1236
+    CONFIG_SETTING_MODE = pytest.mark.parametrize('mode', ['config', 'import'])
1237
+    NO_COLOR = pytest.mark.parametrize(
1238
+        'no_color',
1239
+        [False, True],
1240
+        ids=['yescolor', 'nocolor'],
1241
+    )
1242
+    VAULT_CHARSET_OPTION = pytest.mark.parametrize(
1243
+        'option',
1244
+        [
1245
+            '--lower',
1246
+            '--upper',
1247
+            '--number',
1248
+            '--space',
1249
+            '--dash',
1250
+            '--symbol',
1251
+            '--repeat',
1252
+            '--length',
1253
+        ],
1254
+    )
1255
+    OPTION_COMBINATIONS_INCOMPATIBLE = pytest.mark.parametrize(
1256
+        ['options', 'service'],
1257
+        [
1258
+            pytest.param(o.options, o.needs_service, id=' '.join(o.options))
1259
+            for o in INTERESTING_OPTION_COMBINATIONS
1260
+            if o.incompatible
1261
+        ],
1262
+    )
1263
+    OPTION_COMBINATIONS_SERVICE_NEEDED = pytest.mark.parametrize(
1264
+        ['options', 'service', 'input', 'check_success'],
1265
+        [
1266
+            pytest.param(
1267
+                o.options,
1268
+                o.needs_service,
1269
+                o.input,
1270
+                o.check_success,
1271
+                id=' '.join(o.options),
1272
+            )
1273
+            for o in INTERESTING_OPTION_COMBINATIONS
1274
+            if not o.incompatible
1275
+        ],
1276
+    )
1277
+    COMPLETABLE_ITEMS = pytest.mark.parametrize(
1278
+        ['partial', 'is_completable'],
1279
+        [
1280
+            ('', True),
1281
+            (DUMMY_SERVICE, True),
1282
+            ('a\bn', False),
1283
+            ('\b', False),
1284
+            ('\x00', False),
1285
+            ('\x20', True),
1286
+            ('\x7f', False),
1287
+            ('service with spaces', True),
1288
+            ('service\nwith\nnewlines', False),
1289
+        ],
1290
+    )
1291
+    SHELL_FORMATTER = pytest.mark.parametrize(
1292
+        ['shell', 'format_func'],
1293
+        [
1294
+            pytest.param('bash', bash_format, id='bash'),
1295
+            pytest.param('fish', fish_format, id='fish'),
1296
+            pytest.param('zsh', zsh_format, id='zsh'),
1297
+        ],
1298
+    )
1299
+    TRY_RACE_FREE_IMPLEMENTATION = pytest.mark.parametrize(
1300
+        'try_race_free_implementation', [True, False]
1301
+    )
1302
+    VALIDATION_FUNCTION_INPUT = pytest.mark.parametrize(
1303
+        ['vfunc', 'input'],
1304
+        [
1305
+            (cli_machinery.validate_occurrence_constraint, 20),
1306
+            (cli_machinery.validate_length, 20),
1307
+        ],
1308
+    )
1309
+
1310
+
297 1311
 class TestAllCLI:
298 1312
     """Tests uniformly for all command-line interfaces."""
299 1313
 
... ...
@@ -424,56 +1438,8 @@ class TestAllCLI:
424 1438
             empty_stderr=True, output='Use $VISUAL or $EDITOR to configure'
425 1439
         ), 'expected clean exit, and option group epilog in help text'
426 1440
 
427
-    @pytest.mark.parametrize(
428
-        ['command', 'non_eager_arguments'],
429
-        [
430
-            pytest.param(
431
-                [],
432
-                [],
433
-                id='top-nothing',
434
-            ),
435
-            pytest.param(
436
-                [],
437
-                ['export'],
438
-                id='top-export',
439
-            ),
440
-            pytest.param(
441
-                ['export'],
442
-                [],
443
-                id='export-nothing',
444
-            ),
445
-            pytest.param(
446
-                ['export'],
447
-                ['vault'],
448
-                id='export-vault',
449
-            ),
450
-            pytest.param(
451
-                ['export', 'vault'],
452
-                [],
453
-                id='export-vault-nothing',
454
-            ),
455
-            pytest.param(
456
-                ['export', 'vault'],
457
-                ['--format', 'this-format-doesnt-exist'],
458
-                id='export-vault-args',
459
-            ),
460
-            pytest.param(
461
-                ['vault'],
462
-                [],
463
-                id='vault-nothing',
464
-            ),
465
-            pytest.param(
466
-                ['vault'],
467
-                ['--export', './'],
468
-                id='vault-args',
469
-            ),
470
-        ],
471
-    )
472
-    @pytest.mark.parametrize(
473
-        'arguments',
474
-        [['--help'], ['--version']],
475
-        ids=['help', 'version'],
476
-    )
1441
+    @Parametrizations.COMMAND_NON_EAGER_ARGUMENTS.value
1442
+    @Parametrizations.EAGER_ARGUMENTS.value
477 1443
     def test_200_eager_options(
478 1444
         self,
479 1445
         command: list[str],
... ...
@@ -501,31 +1467,10 @@ class TestAllCLI:
501 1467
             result = tests.ReadableResult.parse(result_)
502 1468
         assert result.clean_exit(empty_stderr=True), 'expected clean exit'
503 1469
 
504
-    @pytest.mark.parametrize(
505
-        'no_color',
506
-        [False, True],
507
-        ids=['yescolor', 'nocolor'],
508
-    )
509
-    @pytest.mark.parametrize(
510
-        'force_color',
511
-        [False, True],
512
-        ids=['noforce', 'force'],
513
-    )
514
-    @pytest.mark.parametrize(
515
-        'isatty',
516
-        [False, True],
517
-        ids=['notty', 'tty'],
518
-    )
519
-    @pytest.mark.parametrize(
520
-        ['command_line', 'input'],
521
-        [
522
-            (
523
-                ['vault', '--import', '-'],
524
-                '{"services": {"": {"length": 20}}}',
525
-            ),
526
-        ],
527
-        ids=['cmd'],
528
-    )
1470
+    @Parametrizations.NO_COLOR.value
1471
+    @Parametrizations.FORCE_COLOR.value
1472
+    @Parametrizations.ISATTY.value
1473
+    @Parametrizations.COLORFUL_COMMAND_INPUT.value
529 1474
     def test_201_no_color_force_color(
530 1475
         self,
531 1476
         no_color: bool,
... ...
@@ -633,9 +1578,7 @@ class TestCLI:
633 1578
             'expected clean exit, and version in help text'
634 1579
         )
635 1580
 
636
-    @pytest.mark.parametrize(
637
-        'charset_name', ['lower', 'upper', 'number', 'space', 'dash', 'symbol']
638
-    )
1581
+    @Parametrizations.CHARSET_NAME.value
639 1582
     def test_201_disable_character_set(
640 1583
         self,
641 1584
         charset_name: str,
... ...
@@ -707,30 +1650,7 @@ class TestCLI:
707 1650
                 f'at position {i}: {result.output!r}'
708 1651
             )
709 1652
 
710
-    @pytest.mark.parametrize(
711
-        'config',
712
-        [
713
-            pytest.param(
714
-                {
715
-                    'global': {'key': DUMMY_KEY1_B64},
716
-                    'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS},
717
-                },
718
-                id='global',
719
-            ),
720
-            pytest.param(
721
-                {
722
-                    'global': {'phrase': DUMMY_PASSPHRASE.rstrip('\n')},
723
-                    'services': {
724
-                        DUMMY_SERVICE: {
725
-                            'key': DUMMY_KEY1_B64,
726
-                            **DUMMY_CONFIG_SETTINGS,
727
-                        }
728
-                    },
729
-                },
730
-                id='service',
731
-            ),
732
-        ],
733
-    )
1653
+    @Parametrizations.CONFIG_WITH_KEY.value
734 1654
     def test_204a_key_from_config(
735 1655
         self,
736 1656
         config: _types.VaultConfig,
... ...
@@ -811,30 +1731,8 @@ class TestCLI:
811 1731
             'expected known output'
812 1732
         )
813 1733
 
814
-    @pytest.mark.parametrize(
815
-        'config',
816
-        [
817
-            pytest.param(
818
-                {
819
-                    'global': {'key': DUMMY_KEY1_B64},
820
-                    'services': {DUMMY_SERVICE: {}},
821
-                },
822
-                id='global_config',
823
-            ),
824
-            pytest.param(
825
-                {'services': {DUMMY_SERVICE: {'key': DUMMY_KEY2_B64}}},
826
-                id='service_config',
827
-            ),
828
-            pytest.param(
829
-                {
830
-                    'global': {'key': DUMMY_KEY1_B64},
831
-                    'services': {DUMMY_SERVICE: {'key': DUMMY_KEY2_B64}},
832
-                },
833
-                id='full_config',
834
-            ),
835
-        ],
836
-    )
837
-    @pytest.mark.parametrize('key_index', [1, 2, 3], ids=lambda i: f'index{i}')
1734
+    @Parametrizations.BASE_CONFIG_WITH_KEY_VARIATIONS.value
1735
+    @Parametrizations.KEY_INDEX.value
838 1736
     def test_204c_key_override_on_command_line(
839 1737
         self,
840 1738
         running_ssh_agent: tests.RunningSSHAgentInfo,
... ...
@@ -920,39 +1818,7 @@ class TestCLI:
920 1818
             'expected known output'
921 1819
         )
922 1820
 
923
-    @pytest.mark.parametrize(
924
-        ['config', 'command_line'],
925
-        [
926
-            pytest.param(
927
-                {
928
-                    'global': {'key': DUMMY_KEY1_B64},
929
-                    'services': {},
930
-                },
931
-                ['--config', '-p'],
932
-                id='global',
933
-            ),
934
-            pytest.param(
935
-                {
936
-                    'services': {
937
-                        DUMMY_SERVICE: {
938
-                            'key': DUMMY_KEY1_B64,
939
-                            **DUMMY_CONFIG_SETTINGS,
940
-                        },
941
-                    },
942
-                },
943
-                ['--config', '-p', '--', DUMMY_SERVICE],
944
-                id='service',
945
-            ),
946
-            pytest.param(
947
-                {
948
-                    'global': {'key': DUMMY_KEY1_B64},
949
-                    'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()},
950
-                },
951
-                ['--config', '-p', '--', DUMMY_SERVICE],
952
-                id='service-over-global',
953
-            ),
954
-        ],
955
-    )
1821
+    @Parametrizations.KEY_OVERRIDING_IN_CONFIG.value
956 1822
     def test_206_setting_phrase_thus_overriding_key_in_config(
957 1823
         self,
958 1824
         running_ssh_agent: tests.RunningSSHAgentInfo,
... ...
@@ -1003,19 +1869,7 @@ class TestCLI:
1003 1869
             map(is_harmless_config_import_warning, caplog.record_tuples)
1004 1870
         ), 'unexpected error output'
1005 1871
 
1006
-    @pytest.mark.parametrize(
1007
-        'option',
1008
-        [
1009
-            '--lower',
1010
-            '--upper',
1011
-            '--number',
1012
-            '--space',
1013
-            '--dash',
1014
-            '--symbol',
1015
-            '--repeat',
1016
-            '--length',
1017
-        ],
1018
-    )
1872
+    @Parametrizations.VAULT_CHARSET_OPTION.value
1019 1873
     def test_210_invalid_argument_range(
1020 1874
         self,
1021 1875
         option: str,
... ...
@@ -1045,20 +1899,7 @@ class TestCLI:
1045 1899
                     'expected error exit and known error message'
1046 1900
                 )
1047 1901
 
1048
-    @pytest.mark.parametrize(
1049
-        ['options', 'service', 'input', 'check_success'],
1050
-        [
1051
-            pytest.param(
1052
-                o.options,
1053
-                o.needs_service,
1054
-                o.input,
1055
-                o.check_success,
1056
-                id=' '.join(o.options),
1057
-            )
1058
-            for o in INTERESTING_OPTION_COMBINATIONS
1059
-            if not o.incompatible
1060
-        ],
1061
-    )
1902
+    @Parametrizations.OPTION_COMBINATIONS_SERVICE_NEEDED.value
1062 1903
     def test_211_service_needed(
1063 1904
         self,
1064 1905
         options: list[str],
... ...
@@ -1197,14 +2038,7 @@ class TestCLI:
1197 2038
                 'services': {'': {'length': 40}},
1198 2039
             }, 'requested configuration change was not applied'
1199 2040
 
1200
-    @pytest.mark.parametrize(
1201
-        ['options', 'service'],
1202
-        [
1203
-            pytest.param(o.options, o.needs_service, id=' '.join(o.options))
1204
-            for o in INTERESTING_OPTION_COMBINATIONS
1205
-            if o.incompatible
1206
-        ],
1207
-    )
2041
+    @Parametrizations.OPTION_COMBINATIONS_INCOMPATIBLE.value
1208 2042
     def test_212_incompatible_options(
1209 2043
         self,
1210 2044
         options: list[str],
... ...
@@ -1234,14 +2068,7 @@ class TestCLI:
1234 2068
             'expected error exit and known error message'
1235 2069
         )
1236 2070
 
1237
-    @pytest.mark.parametrize(
1238
-        'config',
1239
-        [
1240
-            conf.config
1241
-            for conf in TEST_CONFIGS
1242
-            if tests.is_valid_test_config(conf)
1243
-        ],
1244
-    )
2071
+    @Parametrizations.VALID_TEST_CONFIGS.value
1245 2072
     def test_213_import_config_success(
1246 2073
         self,
1247 2074
         caplog: pytest.LogCaptureFixture,
... ...
@@ -1426,13 +2253,7 @@ class TestCLI:
1426 2253
             'expected error exit and known error message'
1427 2254
         )
1428 2255
 
1429
-    @pytest.mark.parametrize(
1430
-        'export_options',
1431
-        [
1432
-            [],
1433
-            ['--export-as=sh'],
1434
-        ],
1435
-    )
2256
+    @Parametrizations.EXPORT_FORMAT_OPTIONS.value
1436 2257
     def test_214_export_settings_no_stored_settings(
1437 2258
         self,
1438 2259
         export_options: list[str],
... ...
@@ -1463,15 +2284,9 @@ class TestCLI:
1463 2284
                 catch_exceptions=False,
1464 2285
             )
1465 2286
         result = tests.ReadableResult.parse(result_)
1466
-        assert result.clean_exit(empty_stderr=True), 'expected clean exit'
1467
-
1468
-    @pytest.mark.parametrize(
1469
-        'export_options',
1470
-        [
1471
-            [],
1472
-            ['--export-as=sh'],
1473
-        ],
1474
-    )
2287
+        assert result.clean_exit(empty_stderr=True), 'expected clean exit'
2288
+
2289
+    @Parametrizations.EXPORT_FORMAT_OPTIONS.value
1475 2290
     def test_214a_export_settings_bad_stored_config(
1476 2291
         self,
1477 2292
         export_options: list[str],
... ...
@@ -1501,13 +2316,7 @@ class TestCLI:
1501 2316
             'expected error exit and known error message'
1502 2317
         )
1503 2318
 
1504
-    @pytest.mark.parametrize(
1505
-        'export_options',
1506
-        [
1507
-            [],
1508
-            ['--export-as=sh'],
1509
-        ],
1510
-    )
2319
+    @Parametrizations.EXPORT_FORMAT_OPTIONS.value
1511 2320
     def test_214b_export_settings_not_a_file(
1512 2321
         self,
1513 2322
         export_options: list[str],
... ...
@@ -1539,13 +2348,7 @@ class TestCLI:
1539 2348
             'expected error exit and known error message'
1540 2349
         )
1541 2350
 
1542
-    @pytest.mark.parametrize(
1543
-        'export_options',
1544
-        [
1545
-            [],
1546
-            ['--export-as=sh'],
1547
-        ],
1548
-    )
2351
+    @Parametrizations.EXPORT_FORMAT_OPTIONS.value
1549 2352
     def test_214c_export_settings_target_not_a_file(
1550 2353
         self,
1551 2354
         export_options: list[str],
... ...
@@ -1575,13 +2378,7 @@ class TestCLI:
1575 2378
             'expected error exit and known error message'
1576 2379
         )
1577 2380
 
1578
-    @pytest.mark.parametrize(
1579
-        'export_options',
1580
-        [
1581
-            [],
1582
-            ['--export-as=sh'],
1583
-        ],
1584
-    )
2381
+    @Parametrizations.EXPORT_FORMAT_OPTIONS.value
1585 2382
     def test_214d_export_settings_settings_directory_not_a_directory(
1586 2383
         self,
1587 2384
         export_options: list[str],
... ...
@@ -1759,53 +2556,7 @@ contents go here
1759 2556
                 config = json.load(infile)
1760 2557
             assert config == {'global': {'phrase': 'abc'}, 'services': {}}
1761 2558
 
1762
-    @pytest.mark.parametrize(
1763
-        ['command_line', 'input', 'result_config'],
1764
-        [
1765
-            pytest.param(
1766
-                ['--phrase'],
1767
-                'my passphrase\n',
1768
-                {'global': {'phrase': 'my passphrase'}, 'services': {}},
1769
-                id='phrase',
1770
-            ),
1771
-            pytest.param(
1772
-                ['--key'],
1773
-                '1\n',
1774
-                {
1775
-                    'global': {'key': DUMMY_KEY1_B64, 'phrase': 'abc'},
1776
-                    'services': {},
1777
-                },
1778
-                id='key',
1779
-            ),
1780
-            pytest.param(
1781
-                ['--phrase', '--', 'sv'],
1782
-                'my passphrase\n',
1783
-                {
1784
-                    'global': {'phrase': 'abc'},
1785
-                    'services': {'sv': {'phrase': 'my passphrase'}},
1786
-                },
1787
-                id='phrase-sv',
1788
-            ),
1789
-            pytest.param(
1790
-                ['--key', '--', 'sv'],
1791
-                '1\n',
1792
-                {
1793
-                    'global': {'phrase': 'abc'},
1794
-                    'services': {'sv': {'key': DUMMY_KEY1_B64}},
1795
-                },
1796
-                id='key-sv',
1797
-            ),
1798
-            pytest.param(
1799
-                ['--key', '--length', '15', '--', 'sv'],
1800
-                '1\n',
1801
-                {
1802
-                    'global': {'phrase': 'abc'},
1803
-                    'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}},
1804
-                },
1805
-                id='key-length-sv',
1806
-            ),
1807
-        ],
1808
-    )
2559
+    @Parametrizations.CONFIG_EDITING_VIA_CONFIG_FLAG.value
1809 2560
     def test_224_store_config_good(
1810 2561
         self,
1811 2562
         command_line: list[str],
... ...
@@ -1845,35 +2596,7 @@ contents go here
1845 2596
                 'stored config does not match expectation'
1846 2597
             )
1847 2598
 
1848
-    @pytest.mark.parametrize(
1849
-        ['command_line', 'input', 'err_text'],
1850
-        [
1851
-            pytest.param(
1852
-                [],
1853
-                '',
1854
-                'Cannot update the global settings without any given settings',
1855
-                id='None',
1856
-            ),
1857
-            pytest.param(
1858
-                ['--', 'sv'],
1859
-                '',
1860
-                'Cannot update the service-specific settings without any given settings',
1861
-                id='None-sv',
1862
-            ),
1863
-            pytest.param(
1864
-                ['--phrase', '--', 'sv'],
1865
-                '',
1866
-                'No passphrase was given',
1867
-                id='phrase-sv',
1868
-            ),
1869
-            pytest.param(
1870
-                ['--key'],
1871
-                '',
1872
-                'No SSH key was selected',
1873
-                id='key-sv',
1874
-            ),
1875
-        ],
1876
-    )
2599
+    @Parametrizations.CONFIG_EDITING_VIA_CONFIG_FLAG_FAILURES.value
1877 2600
     def test_225_store_config_fail(
1878 2601
         self,
1879 2602
         command_line: list[str],
... ...
@@ -2000,7 +2723,7 @@ contents go here
2000 2723
             'expected error exit and known error message'
2001 2724
         )
2002 2725
 
2003
-    @pytest.mark.parametrize('try_race_free_implementation', [True, False])
2726
+    @Parametrizations.TRY_RACE_FREE_IMPLEMENTATION.value
2004 2727
     def test_225d_store_config_fail_manual_read_only_file(
2005 2728
         self,
2006 2729
         try_race_free_implementation: bool,
... ...
@@ -2317,170 +3040,72 @@ contents go here
2317 3040
         # with-statements.
2318 3041
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2319 3042
         with contextlib.ExitStack() as stack:
2320
-            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2321
-            stack.enter_context(
2322
-                tests.isolated_config(
2323
-                    monkeypatch=monkeypatch,
2324
-                    runner=runner,
2325
-                )
2326
-            )
2327
-            save_config_ = cli_helpers.save_config
2328
-
2329
-            def obstruct_config_saving(*args: Any, **kwargs: Any) -> Any:
2330
-                config_dir = cli_helpers.config_filename(subsystem=None)
2331
-                with contextlib.suppress(FileNotFoundError):
2332
-                    shutil.rmtree(config_dir)
2333
-                config_dir.write_text('Obstruction!!\n')
2334
-                monkeypatch.setattr(cli_helpers, 'save_config', save_config_)
2335
-                return save_config_(*args, **kwargs)
2336
-
2337
-            monkeypatch.setattr(
2338
-                cli_helpers, 'save_config', obstruct_config_saving
2339
-            )
2340
-            result_ = runner.invoke(
2341
-                cli.derivepassphrase_vault,
2342
-                ['--config', '-p'],
2343
-                catch_exceptions=False,
2344
-                input='abc\n',
2345
-            )
2346
-            result = tests.ReadableResult.parse(result_)
2347
-            assert result.error_exit(error='Cannot store vault settings:'), (
2348
-                'expected error exit and known error message'
2349
-            )
2350
-
2351
-    def test_230b_store_config_custom_error(
2352
-        self,
2353
-    ) -> None:
2354
-        """Storing the configuration reacts even to weird errors."""
2355
-        runner = click.testing.CliRunner(mix_stderr=False)
2356
-        # TODO(the-13th-letter): Rewrite using parenthesized
2357
-        # with-statements.
2358
-        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2359
-        with contextlib.ExitStack() as stack:
2360
-            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2361
-            stack.enter_context(
2362
-                tests.isolated_config(
2363
-                    monkeypatch=monkeypatch,
2364
-                    runner=runner,
2365
-                )
2366
-            )
2367
-            custom_error = 'custom error message'
2368
-
2369
-            def raiser(config: Any) -> None:
2370
-                del config
2371
-                raise RuntimeError(custom_error)
2372
-
2373
-            monkeypatch.setattr(cli_helpers, 'save_config', raiser)
2374
-            result_ = runner.invoke(
2375
-                cli.derivepassphrase_vault,
2376
-                ['--config', '-p'],
2377
-                catch_exceptions=False,
2378
-                input='abc\n',
2379
-            )
2380
-            result = tests.ReadableResult.parse(result_)
2381
-            assert result.error_exit(error=custom_error), (
2382
-                'expected error exit and known error message'
2383
-            )
2384
-
2385
-    @pytest.mark.parametrize(
2386
-        ['main_config', 'command_line', 'input', 'warning_message'],
2387
-        [
2388
-            pytest.param(
2389
-                '',
2390
-                ['--import', '-'],
2391
-                json.dumps({
2392
-                    'global': {'phrase': 'Du\u0308sseldorf'},
2393
-                    'services': {},
2394
-                }),
2395
-                'The $.global passphrase is not NFC-normalized',
2396
-                id='global-NFC',
2397
-            ),
2398
-            pytest.param(
2399
-                '',
2400
-                ['--import', '-'],
2401
-                json.dumps({
2402
-                    'services': {
2403
-                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
2404
-                        'weird entry name': {'phrase': 'Du\u0308sseldorf'},
2405
-                    }
2406
-                }),
2407
-                (
2408
-                    'The $.services["weird entry name"] passphrase '
2409
-                    'is not NFC-normalized'
2410
-                ),
2411
-                id='service-weird-name-NFC',
2412
-            ),
2413
-            pytest.param(
2414
-                '',
2415
-                ['--config', '-p', '--', DUMMY_SERVICE],
2416
-                'Du\u0308sseldorf',
2417
-                (
2418
-                    f'The $.services.{DUMMY_SERVICE} passphrase '
2419
-                    f'is not NFC-normalized'
2420
-                ),
2421
-                id='config-NFC',
2422
-            ),
2423
-            pytest.param(
2424
-                '',
2425
-                ['-p', '--', DUMMY_SERVICE],
2426
-                'Du\u0308sseldorf',
2427
-                'The interactive input passphrase is not NFC-normalized',
2428
-                id='direct-input-NFC',
2429
-            ),
2430
-            pytest.param(
2431
-                textwrap.dedent(r"""
2432
-                [vault]
2433
-                default-unicode-normalization-form = 'NFD'
2434
-                """),
2435
-                ['--import', '-'],
2436
-                json.dumps({
2437
-                    'global': {
2438
-                        'phrase': 'D\u00fcsseldorf',
2439
-                    },
2440
-                    'services': {},
2441
-                }),
2442
-                'The $.global passphrase is not NFD-normalized',
2443
-                id='global-NFD',
2444
-            ),
2445
-            pytest.param(
2446
-                textwrap.dedent(r"""
2447
-                [vault]
2448
-                default-unicode-normalization-form = 'NFD'
2449
-                """),
2450
-                ['--import', '-'],
2451
-                json.dumps({
2452
-                    'services': {
2453
-                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
2454
-                        'weird entry name': {'phrase': 'D\u00fcsseldorf'},
2455
-                    },
2456
-                }),
2457
-                (
2458
-                    'The $.services["weird entry name"] passphrase '
2459
-                    'is not NFD-normalized'
2460
-                ),
2461
-                id='service-weird-name-NFD',
2462
-            ),
2463
-            pytest.param(
2464
-                textwrap.dedent(r"""
2465
-                [vault.unicode-normalization-form]
2466
-                'weird entry name 2' = 'NFKD'
2467
-                """),
2468
-                ['--import', '-'],
2469
-                json.dumps({
2470
-                    'services': {
2471
-                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
2472
-                        'weird entry name 1': {'phrase': 'D\u00fcsseldorf'},
2473
-                        'weird entry name 2': {'phrase': 'D\u00fcsseldorf'},
2474
-                    },
2475
-                }),
2476
-                (
2477
-                    'The $.services["weird entry name 2"] passphrase '
2478
-                    'is not NFKD-normalized'
2479
-                ),
2480
-                id='service-weird-name-2-NFKD',
2481
-            ),
2482
-        ],
3043
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3044
+            stack.enter_context(
3045
+                tests.isolated_config(
3046
+                    monkeypatch=monkeypatch,
3047
+                    runner=runner,
3048
+                )
3049
+            )
3050
+            save_config_ = cli_helpers.save_config
3051
+
3052
+            def obstruct_config_saving(*args: Any, **kwargs: Any) -> Any:
3053
+                config_dir = cli_helpers.config_filename(subsystem=None)
3054
+                with contextlib.suppress(FileNotFoundError):
3055
+                    shutil.rmtree(config_dir)
3056
+                config_dir.write_text('Obstruction!!\n')
3057
+                monkeypatch.setattr(cli_helpers, 'save_config', save_config_)
3058
+                return save_config_(*args, **kwargs)
3059
+
3060
+            monkeypatch.setattr(
3061
+                cli_helpers, 'save_config', obstruct_config_saving
3062
+            )
3063
+            result_ = runner.invoke(
3064
+                cli.derivepassphrase_vault,
3065
+                ['--config', '-p'],
3066
+                catch_exceptions=False,
3067
+                input='abc\n',
3068
+            )
3069
+            result = tests.ReadableResult.parse(result_)
3070
+            assert result.error_exit(error='Cannot store vault settings:'), (
3071
+                'expected error exit and known error message'
3072
+            )
3073
+
3074
+    def test_230b_store_config_custom_error(
3075
+        self,
3076
+    ) -> None:
3077
+        """Storing the configuration reacts even to weird errors."""
3078
+        runner = click.testing.CliRunner(mix_stderr=False)
3079
+        # TODO(the-13th-letter): Rewrite using parenthesized
3080
+        # with-statements.
3081
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3082
+        with contextlib.ExitStack() as stack:
3083
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3084
+            stack.enter_context(
3085
+                tests.isolated_config(
3086
+                    monkeypatch=monkeypatch,
3087
+                    runner=runner,
3088
+                )
3089
+            )
3090
+            custom_error = 'custom error message'
3091
+
3092
+            def raiser(config: Any) -> None:
3093
+                del config
3094
+                raise RuntimeError(custom_error)
3095
+
3096
+            monkeypatch.setattr(cli_helpers, 'save_config', raiser)
3097
+            result_ = runner.invoke(
3098
+                cli.derivepassphrase_vault,
3099
+                ['--config', '-p'],
3100
+                catch_exceptions=False,
3101
+                input='abc\n',
3102
+            )
3103
+            result = tests.ReadableResult.parse(result_)
3104
+            assert result.error_exit(error=custom_error), (
3105
+                'expected error exit and known error message'
2483 3106
             )
3107
+
3108
+    @Parametrizations.UNICODE_NORMALIZATION_WARNING_INPUTS.value
2484 3109
     def test_300_unicode_normalization_form_warning(
2485 3110
         self,
2486 3111
         caplog: pytest.LogCaptureFixture,
... ...
@@ -2520,47 +3145,7 @@ contents go here
2520 3145
             'expected known warning message in stderr'
2521 3146
         )
2522 3147
 
2523
-    @pytest.mark.parametrize(
2524
-        ['main_config', 'command_line', 'input', 'error_message'],
2525
-        [
2526
-            pytest.param(
2527
-                textwrap.dedent(r"""
2528
-                [vault]
2529
-                default-unicode-normalization-form = 'XXX'
2530
-                """),
2531
-                ['--import', '-'],
2532
-                json.dumps({
2533
-                    'services': {
2534
-                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
2535
-                        'with_normalization': {'phrase': 'D\u00fcsseldorf'},
2536
-                    },
2537
-                }),
2538
-                (
2539
-                    "Invalid value 'XXX' for config key "
2540
-                    'vault.default-unicode-normalization-form'
2541
-                ),
2542
-                id='global',
2543
-            ),
2544
-            pytest.param(
2545
-                textwrap.dedent(r"""
2546
-                [vault.unicode-normalization-form]
2547
-                with_normalization = 'XXX'
2548
-                """),
2549
-                ['--import', '-'],
2550
-                json.dumps({
2551
-                    'services': {
2552
-                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
2553
-                        'with_normalization': {'phrase': 'D\u00fcsseldorf'},
2554
-                    },
2555
-                }),
2556
-                (
2557
-                    "Invalid value 'XXX' for config key "
2558
-                    'vault.with_normalization.unicode-normalization-form'
2559
-                ),
2560
-                id='service',
2561
-            ),
2562
-        ],
2563
-    )
3148
+    @Parametrizations.UNICODE_NORMALIZATION_ERROR_INPUTS.value
2564 3149
     def test_301_unicode_normalization_form_error(
2565 3150
         self,
2566 3151
         main_config: str,
... ...
@@ -2601,19 +3186,7 @@ contents go here
2601 3186
             'expected error exit and known error message'
2602 3187
         )
2603 3188
 
2604
-    @pytest.mark.parametrize(
2605
-        'command_line',
2606
-        [
2607
-            pytest.param(
2608
-                ['--config', '--phrase'],
2609
-                id='configure global passphrase',
2610
-            ),
2611
-            pytest.param(
2612
-                ['--phrase', '--', DUMMY_SERVICE],
2613
-                id='interactive passphrase',
2614
-            ),
2615
-        ],
2616
-    )
3189
+    @Parametrizations.UNICODE_NORMALIZATION_COMMAND_LINES.value
2617 3190
     def test_301a_unicode_normalization_form_error_from_stored_config(
2618 3191
         self,
2619 3192
         command_line: list[str],
... ...
@@ -2720,25 +3293,7 @@ contents go here
2720 3293
 class TestCLIUtils:
2721 3294
     """Tests for command-line utility functions."""
2722 3295
 
2723
-    @pytest.mark.parametrize(
2724
-        'config',
2725
-        [
2726
-            {'global': {'phrase': 'my passphrase'}, 'services': {}},
2727
-            {'global': {'key': DUMMY_KEY1_B64}, 'services': {}},
2728
-            {
2729
-                'global': {'phrase': 'abc'},
2730
-                'services': {'sv': {'phrase': 'my passphrase'}},
2731
-            },
2732
-            {
2733
-                'global': {'phrase': 'abc'},
2734
-                'services': {'sv': {'key': DUMMY_KEY1_B64}},
2735
-            },
2736
-            {
2737
-                'global': {'phrase': 'abc'},
2738
-                'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}},
2739
-            },
2740
-        ],
2741
-    )
3296
+    @Parametrizations.BASE_CONFIG_VARIATIONS.value
2742 3297
     def test_100_load_config(
2743 3298
         self,
2744 3299
         config: Any,
... ...
@@ -3300,35 +3855,7 @@ Boo.
3300 3855
         assert _types.is_vault_config(config)
3301 3856
         return self.export_as_sh_helper(config)
3302 3857
 
3303
-    @pytest.mark.parametrize(
3304
-        ['command_line', 'config', 'result_config'],
3305
-        [
3306
-            pytest.param(
3307
-                ['--delete-globals'],
3308
-                {'global': {'phrase': 'abc'}, 'services': {}},
3309
-                {'services': {}},
3310
-                id='globals',
3311
-            ),
3312
-            pytest.param(
3313
-                ['--delete', '--', DUMMY_SERVICE],
3314
-                {
3315
-                    'global': {'phrase': 'abc'},
3316
-                    'services': {DUMMY_SERVICE: {'notes': '...'}},
3317
-                },
3318
-                {'global': {'phrase': 'abc'}, 'services': {}},
3319
-                id='service',
3320
-            ),
3321
-            pytest.param(
3322
-                ['--clear'],
3323
-                {
3324
-                    'global': {'phrase': 'abc'},
3325
-                    'services': {DUMMY_SERVICE: {'notes': '...'}},
3326
-                },
3327
-                {'services': {}},
3328
-                id='all',
3329
-            ),
3330
-        ],
3331
-    )
3858
+    @Parametrizations.DELETE_CONFIG_INPUT.value
3332 3859
     def test_203_repeated_config_deletion(
3333 3860
         self,
3334 3861
         command_line: list[str],
... ...
@@ -3374,13 +3901,7 @@ Boo.
3374 3901
             == DUMMY_RESULT_KEY1
3375 3902
         )
3376 3903
 
3377
-    @pytest.mark.parametrize(
3378
-        ['vfunc', 'input'],
3379
-        [
3380
-            (cli_machinery.validate_occurrence_constraint, 20),
3381
-            (cli_machinery.validate_length, 20),
3382
-        ],
3383
-    )
3904
+    @Parametrizations.VALIDATION_FUNCTION_INPUT.value
3384 3905
     def test_210a_validate_constraints_manually(
3385 3906
         self,
3386 3907
         vfunc: Callable[[click.Context, click.Parameter, Any], int | None],
... ...
@@ -3391,7 +3912,7 @@ Boo.
3391 3912
         param = cli.derivepassphrase_vault.params[0]
3392 3913
         assert vfunc(ctx, param, input) == input
3393 3914
 
3394
-    @pytest.mark.parametrize('conn_hint', ['none', 'socket', 'client'])
3915
+    @Parametrizations.CONNECTION_HINTS.value
3395 3916
     def test_227_get_suitable_ssh_keys(
3396 3917
         self,
3397 3918
         running_ssh_agent: tests.RunningSSHAgentInfo,
... ...
@@ -3522,25 +4043,7 @@ Boo.
3522 4043
 class TestCLITransition:
3523 4044
     """Transition tests for the command-line interface up to v1.0."""
3524 4045
 
3525
-    @pytest.mark.parametrize(
3526
-        'config',
3527
-        [
3528
-            {'global': {'phrase': 'my passphrase'}, 'services': {}},
3529
-            {'global': {'key': DUMMY_KEY1_B64}, 'services': {}},
3530
-            {
3531
-                'global': {'phrase': 'abc'},
3532
-                'services': {'sv': {'phrase': 'my passphrase'}},
3533
-            },
3534
-            {
3535
-                'global': {'phrase': 'abc'},
3536
-                'services': {'sv': {'key': DUMMY_KEY1_B64}},
3537
-            },
3538
-            {
3539
-                'global': {'phrase': 'abc'},
3540
-                'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}},
3541
-            },
3542
-        ],
3543
-    )
4046
+    @Parametrizations.BASE_CONFIG_VARIATIONS.value
3544 4047
     def test_110_load_config_backup(
3545 4048
         self,
3546 4049
         config: Any,
... ...
@@ -3563,25 +4066,7 @@ class TestCLITransition:
3563 4066
             ).write_text(json.dumps(config, indent=2) + '\n', encoding='UTF-8')
3564 4067
             assert cli_helpers.migrate_and_load_old_config()[0] == config
3565 4068
 
3566
-    @pytest.mark.parametrize(
3567
-        'config',
3568
-        [
3569
-            {'global': {'phrase': 'my passphrase'}, 'services': {}},
3570
-            {'global': {'key': DUMMY_KEY1_B64}, 'services': {}},
3571
-            {
3572
-                'global': {'phrase': 'abc'},
3573
-                'services': {'sv': {'phrase': 'my passphrase'}},
3574
-            },
3575
-            {
3576
-                'global': {'phrase': 'abc'},
3577
-                'services': {'sv': {'key': DUMMY_KEY1_B64}},
3578
-            },
3579
-            {
3580
-                'global': {'phrase': 'abc'},
3581
-                'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}},
3582
-            },
3583
-        ],
3584
-    )
4069
+    @Parametrizations.BASE_CONFIG_VARIATIONS.value
3585 4070
     def test_111_migrate_config(
3586 4071
         self,
3587 4072
         config: Any,
... ...
@@ -3604,25 +4089,7 @@ class TestCLITransition:
3604 4089
             ).write_text(json.dumps(config, indent=2) + '\n', encoding='UTF-8')
3605 4090
             assert cli_helpers.migrate_and_load_old_config() == (config, None)
3606 4091
 
3607
-    @pytest.mark.parametrize(
3608
-        'config',
3609
-        [
3610
-            {'global': {'phrase': 'my passphrase'}, 'services': {}},
3611
-            {'global': {'key': DUMMY_KEY1_B64}, 'services': {}},
3612
-            {
3613
-                'global': {'phrase': 'abc'},
3614
-                'services': {'sv': {'phrase': 'my passphrase'}},
3615
-            },
3616
-            {
3617
-                'global': {'phrase': 'abc'},
3618
-                'services': {'sv': {'key': DUMMY_KEY1_B64}},
3619
-            },
3620
-            {
3621
-                'global': {'phrase': 'abc'},
3622
-                'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}},
3623
-            },
3624
-        ],
3625
-    )
4092
+    @Parametrizations.BASE_CONFIG_VARIATIONS.value
3626 4093
     def test_112_migrate_config_error(
3627 4094
         self,
3628 4095
         config: Any,
... ...
@@ -3634,42 +4101,24 @@ class TestCLITransition:
3634 4101
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3635 4102
         with contextlib.ExitStack() as stack:
3636 4103
             monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3637
-            stack.enter_context(
3638
-                tests.isolated_config(
3639
-                    monkeypatch=monkeypatch,
3640
-                    runner=runner,
3641
-                )
3642
-            )
3643
-            cli_helpers.config_filename(
3644
-                subsystem='old settings.json'
3645
-            ).write_text(json.dumps(config, indent=2) + '\n', encoding='UTF-8')
3646
-            cli_helpers.config_filename(subsystem='vault').mkdir(
3647
-                parents=True, exist_ok=True
3648
-            )
3649
-            config2, err = cli_helpers.migrate_and_load_old_config()
3650
-            assert config2 == config
3651
-            assert isinstance(err, OSError)
3652
-            assert err.errno == errno.EISDIR
3653
-
3654
-    @pytest.mark.parametrize(
3655
-        'config',
3656
-        [
3657
-            {'global': '', 'services': {}},
3658
-            {'global': 0, 'services': {}},
3659
-            {
3660
-                'global': {'phrase': 'abc'},
3661
-                'services': False,
3662
-            },
3663
-            {
3664
-                'global': {'phrase': 'abc'},
3665
-                'services': True,
3666
-            },
3667
-            {
3668
-                'global': {'phrase': 'abc'},
3669
-                'services': None,
3670
-            },
3671
-        ],
4104
+            stack.enter_context(
4105
+                tests.isolated_config(
4106
+                    monkeypatch=monkeypatch,
4107
+                    runner=runner,
4108
+                )
4109
+            )
4110
+            cli_helpers.config_filename(
4111
+                subsystem='old settings.json'
4112
+            ).write_text(json.dumps(config, indent=2) + '\n', encoding='UTF-8')
4113
+            cli_helpers.config_filename(subsystem='vault').mkdir(
4114
+                parents=True, exist_ok=True
3672 4115
             )
4116
+            config2, err = cli_helpers.migrate_and_load_old_config()
4117
+            assert config2 == config
4118
+            assert isinstance(err, OSError)
4119
+            assert err.errno == errno.EISDIR
4120
+
4121
+    @Parametrizations.BAD_CONFIGS.value
3673 4122
     def test_113_migrate_config_error_bad_config_value(
3674 4123
         self,
3675 4124
         config: Any,
... ...
@@ -3763,9 +4212,7 @@ class TestCLITransition:
3763 4212
             'expected error exit and known error type'
3764 4213
         )
3765 4214
 
3766
-    @pytest.mark.parametrize(
3767
-        'charset_name', ['lower', 'upper', 'number', 'space', 'dash', 'symbol']
3768
-    )
4215
+    @Parametrizations.CHARSET_NAME.value
3769 4216
     def test_210_forward_vault_disable_character_set(
3770 4217
         self,
3771 4218
         caplog: pytest.LogCaptureFixture,
... ...
@@ -4468,57 +4915,6 @@ TestConfigManagement = ConfigManagementStateMachine.TestCase
4468 4915
 """The [`unittest.TestCase`][] class that will actually be run."""
4469 4916
 
4470 4917
 
4471
-def bash_format(item: click.shell_completion.CompletionItem) -> str:
4472
-    """A formatter for `bash`-style shell completion items.
4473
-
4474
-    The format is `type,value`, and is dictated by [`click`][].
4475
-
4476
-    """
4477
-    type, value = (  # noqa: A001
4478
-        item.type,
4479
-        item.value,
4480
-    )
4481
-    return f'{type},{value}'
4482
-
4483
-
4484
-def fish_format(item: click.shell_completion.CompletionItem) -> str:
4485
-    r"""A formatter for `fish`-style shell completion items.
4486
-
4487
-    The format is `type,value<tab>help`, and is dictated by [`click`][].
4488
-
4489
-    """
4490
-    type, value, help = (  # noqa: A001
4491
-        item.type,
4492
-        item.value,
4493
-        item.help,
4494
-    )
4495
-    return f'{type},{value}\t{help}' if help else f'{type},{value}'
4496
-
4497
-
4498
-def zsh_format(item: click.shell_completion.CompletionItem) -> str:
4499
-    r"""A formatter for `zsh`-style shell completion items.
4500
-
4501
-    The format is `type<newline>value<newline>help<newline>`, and is
4502
-    dictated by [`click`][].  Upstream `click` currently (v8.2.0) does
4503
-    not deal with colons in the value correctly when the help text is
4504
-    non-degenerate.  Our formatter here does, provided the upstream
4505
-    `zsh` completion script is used; see the
4506
-    [`cli_machinery.ZshComplete`][] class.  A request is underway to
4507
-    merge this change into upstream `click`; see
4508
-    [`pallets/click#2846`][PR2846].
4509
-
4510
-    [PR2846]: https://github.com/pallets/click/pull/2846
4511
-
4512
-    """
4513
-    empty_help = '_'
4514
-    help_, value = (
4515
-        (item.help, item.value.replace(':', r'\:'))
4516
-        if item.help and item.help == empty_help
4517
-        else (empty_help, item.value)
4518
-    )
4519
-    return f'{item.type}\n{value}\n{help_}'
4520
-
4521
-
4522 4918
 def completion_item(
4523 4919
     item: str | click.shell_completion.CompletionItem,
4524 4920
 ) -> click.shell_completion.CompletionItem:
... ...
@@ -4581,20 +4977,7 @@ class TestShellCompletion:
4581 4977
             """Return the completion items' values, as a sequence."""
4582 4978
             return tuple(c.value for c in self())
4583 4979
 
4584
-    @pytest.mark.parametrize(
4585
-        ['partial', 'is_completable'],
4586
-        [
4587
-            ('', True),
4588
-            (DUMMY_SERVICE, True),
4589
-            ('a\bn', False),
4590
-            ('\b', False),
4591
-            ('\x00', False),
4592
-            ('\x20', True),
4593
-            ('\x7f', False),
4594
-            ('service with spaces', True),
4595
-            ('service\nwith\nnewlines', False),
4596
-        ],
4597
-    )
4980
+    @Parametrizations.COMPLETABLE_ITEMS.value
4598 4981
     def test_100_is_completable_item(
4599 4982
         self,
4600 4983
         partial: str,
... ...
@@ -4603,106 +4986,7 @@ class TestShellCompletion:
4603 4986
         """Our `_is_completable_item` predicate for service names works."""
4604 4987
         assert cli_helpers.is_completable_item(partial) == is_completable
4605 4988
 
4606
-    @pytest.mark.parametrize(
4607
-        ['command_prefix', 'incomplete', 'completions'],
4608
-        [
4609
-            pytest.param(
4610
-                (),
4611
-                '-',
4612
-                frozenset({
4613
-                    '--help',
4614
-                    '-h',
4615
-                    '--version',
4616
-                    '--debug',
4617
-                    '--verbose',
4618
-                    '-v',
4619
-                    '--quiet',
4620
-                    '-q',
4621
-                }),
4622
-                id='derivepassphrase',
4623
-            ),
4624
-            pytest.param(
4625
-                ('export',),
4626
-                '-',
4627
-                frozenset({
4628
-                    '--help',
4629
-                    '-h',
4630
-                    '--version',
4631
-                    '--debug',
4632
-                    '--verbose',
4633
-                    '-v',
4634
-                    '--quiet',
4635
-                    '-q',
4636
-                }),
4637
-                id='derivepassphrase-export',
4638
-            ),
4639
-            pytest.param(
4640
-                ('export', 'vault'),
4641
-                '-',
4642
-                frozenset({
4643
-                    '--help',
4644
-                    '-h',
4645
-                    '--version',
4646
-                    '--debug',
4647
-                    '--verbose',
4648
-                    '-v',
4649
-                    '--quiet',
4650
-                    '-q',
4651
-                    '--format',
4652
-                    '-f',
4653
-                    '--key',
4654
-                    '-k',
4655
-                }),
4656
-                id='derivepassphrase-export-vault',
4657
-            ),
4658
-            pytest.param(
4659
-                ('vault',),
4660
-                '-',
4661
-                frozenset({
4662
-                    '--help',
4663
-                    '-h',
4664
-                    '--version',
4665
-                    '--debug',
4666
-                    '--verbose',
4667
-                    '-v',
4668
-                    '--quiet',
4669
-                    '-q',
4670
-                    '--phrase',
4671
-                    '-p',
4672
-                    '--key',
4673
-                    '-k',
4674
-                    '--length',
4675
-                    '-l',
4676
-                    '--repeat',
4677
-                    '-r',
4678
-                    '--upper',
4679
-                    '--lower',
4680
-                    '--number',
4681
-                    '--space',
4682
-                    '--dash',
4683
-                    '--symbol',
4684
-                    '--config',
4685
-                    '-c',
4686
-                    '--notes',
4687
-                    '-n',
4688
-                    '--delete',
4689
-                    '-x',
4690
-                    '--delete-globals',
4691
-                    '--clear',
4692
-                    '-X',
4693
-                    '--export',
4694
-                    '-e',
4695
-                    '--import',
4696
-                    '-i',
4697
-                    '--overwrite-existing',
4698
-                    '--merge-existing',
4699
-                    '--unset',
4700
-                    '--export-as',
4701
-                }),
4702
-                id='derivepassphrase-vault',
4703
-            ),
4704
-        ],
4705
-    )
4989
+    @Parametrizations.COMPLETABLE_OPTIONS.value
4706 4990
     def test_200_options(
4707 4991
         self,
4708 4992
         command_prefix: Sequence[str],
... ...
@@ -4713,23 +4997,7 @@ class TestShellCompletion:
4713 4997
         comp = self.Completions(command_prefix, incomplete)
4714 4998
         assert frozenset(comp.get_words()) == completions
4715 4999
 
4716
-    @pytest.mark.parametrize(
4717
-        ['command_prefix', 'incomplete', 'completions'],
4718
-        [
4719
-            pytest.param(
4720
-                (),
4721
-                '',
4722
-                frozenset({'export', 'vault'}),
4723
-                id='derivepassphrase',
4724
-            ),
4725
-            pytest.param(
4726
-                ('export',),
4727
-                '',
4728
-                frozenset({'vault'}),
4729
-                id='derivepassphrase-export',
4730
-            ),
4731
-        ],
4732
-    )
5000
+    @Parametrizations.COMPLETABLE_SUBCOMMANDS.value
4733 5001
     def test_201_subcommands(
4734 5002
         self,
4735 5003
         command_prefix: Sequence[str],
... ...
@@ -4740,72 +5008,22 @@ class TestShellCompletion:
4740 5008
         comp = self.Completions(command_prefix, incomplete)
4741 5009
         assert frozenset(comp.get_words()) == completions
4742 5010
 
4743
-    @pytest.mark.parametrize(
4744
-        'command_prefix',
4745
-        [
4746
-            pytest.param(
4747
-                ('export', 'vault'),
4748
-                id='derivepassphrase-export-vault',
4749
-            ),
4750
-            pytest.param(
4751
-                ('vault', '--export'),
4752
-                id='derivepassphrase-vault--export',
4753
-            ),
4754
-            pytest.param(
4755
-                ('vault', '--import'),
4756
-                id='derivepassphrase-vault--import',
4757
-            ),
4758
-        ],
4759
-    )
4760
-    @pytest.mark.parametrize('incomplete', ['', 'partial'])
5011
+    @Parametrizations.COMPLETABLE_PATH_ARGUMENT.value
5012
+    @Parametrizations.INCOMPLETE.value
4761 5013
     def test_202_paths(
4762 5014
         self,
4763 5015
         command_prefix: Sequence[str],
4764
-        incomplete: str,
4765
-    ) -> None:
4766
-        """Our completion machinery works for all commands' paths."""
4767
-        file = click.shell_completion.CompletionItem('', type='file')
4768
-        completions = frozenset({(file.type, file.value, file.help)})
4769
-        comp = self.Completions(command_prefix, incomplete)
4770
-        assert (
4771
-            frozenset((x.type, x.value, x.help) for x in comp()) == completions
4772
-        )
4773
-
4774
-    @pytest.mark.parametrize(
4775
-        ['config', 'incomplete', 'completions'],
4776
-        [
4777
-            pytest.param(
4778
-                {'services': {}},
4779
-                '',
4780
-                frozenset(),
4781
-                id='no_services',
4782
-            ),
4783
-            pytest.param(
4784
-                {'services': {}},
4785
-                'partial',
4786
-                frozenset(),
4787
-                id='no_services_partial',
4788
-            ),
4789
-            pytest.param(
4790
-                {'services': {DUMMY_SERVICE: {'length': 10}}},
4791
-                '',
4792
-                frozenset({DUMMY_SERVICE}),
4793
-                id='one_service',
4794
-            ),
4795
-            pytest.param(
4796
-                {'services': {DUMMY_SERVICE: {'length': 10}}},
4797
-                DUMMY_SERVICE[:4],
4798
-                frozenset({DUMMY_SERVICE}),
4799
-                id='one_service_partial',
4800
-            ),
4801
-            pytest.param(
4802
-                {'services': {DUMMY_SERVICE: {'length': 10}}},
4803
-                DUMMY_SERVICE[-4:],
4804
-                frozenset(),
4805
-                id='one_service_partial_miss',
4806
-            ),
4807
-        ],
5016
+        incomplete: str,
5017
+    ) -> None:
5018
+        """Our completion machinery works for all commands' paths."""
5019
+        file = click.shell_completion.CompletionItem('', type='file')
5020
+        completions = frozenset({(file.type, file.value, file.help)})
5021
+        comp = self.Completions(command_prefix, incomplete)
5022
+        assert (
5023
+            frozenset((x.type, x.value, x.help) for x in comp()) == completions
4808 5024
         )
5025
+
5026
+    @Parametrizations.COMPLETABLE_SERVICE_NAMES.value
4809 5027
     def test_203_service_names(
4810 5028
         self,
4811 5029
         config: _types.VaultConfig,
... ...
@@ -4829,107 +5047,8 @@ class TestShellCompletion:
4829 5047
             comp = self.Completions(['vault'], incomplete)
4830 5048
             assert frozenset(comp.get_words()) == completions
4831 5049
 
4832
-    @pytest.mark.parametrize(
4833
-        ['shell', 'format_func'],
4834
-        [
4835
-            pytest.param('bash', bash_format, id='bash'),
4836
-            pytest.param('fish', fish_format, id='fish'),
4837
-            pytest.param('zsh', zsh_format, id='zsh'),
4838
-        ],
4839
-    )
4840
-    @pytest.mark.parametrize(
4841
-        ['config', 'comp_func', 'args', 'incomplete', 'results'],
4842
-        [
4843
-            pytest.param(
4844
-                {'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}},
4845
-                cli_helpers.shell_complete_service,
4846
-                ['vault'],
4847
-                '',
4848
-                [DUMMY_SERVICE],
4849
-                id='base_config-service',
4850
-            ),
4851
-            pytest.param(
4852
-                {'services': {}},
4853
-                cli_helpers.shell_complete_service,
4854
-                ['vault'],
4855
-                '',
4856
-                [],
4857
-                id='empty_config-service',
4858
-            ),
4859
-            pytest.param(
4860
-                {
4861
-                    'services': {
4862
-                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
4863
-                        'newline\nin\nname': DUMMY_CONFIG_SETTINGS.copy(),
4864
-                    }
4865
-                },
4866
-                cli_helpers.shell_complete_service,
4867
-                ['vault'],
4868
-                '',
4869
-                [DUMMY_SERVICE],
4870
-                id='incompletable_newline_config-service',
4871
-            ),
4872
-            pytest.param(
4873
-                {
4874
-                    'services': {
4875
-                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
4876
-                        'backspace\bin\bname': DUMMY_CONFIG_SETTINGS.copy(),
4877
-                    }
4878
-                },
4879
-                cli_helpers.shell_complete_service,
4880
-                ['vault'],
4881
-                '',
4882
-                [DUMMY_SERVICE],
4883
-                id='incompletable_backspace_config-service',
4884
-            ),
4885
-            pytest.param(
4886
-                {
4887
-                    'services': {
4888
-                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
4889
-                        'colon:in:name': DUMMY_CONFIG_SETTINGS.copy(),
4890
-                    }
4891
-                },
4892
-                cli_helpers.shell_complete_service,
4893
-                ['vault'],
4894
-                '',
4895
-                sorted([DUMMY_SERVICE, 'colon:in:name']),
4896
-                id='brittle_colon_config-service',
4897
-            ),
4898
-            pytest.param(
4899
-                {
4900
-                    'services': {
4901
-                        DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
4902
-                        'colon:in:name': DUMMY_CONFIG_SETTINGS.copy(),
4903
-                        'newline\nin\nname': DUMMY_CONFIG_SETTINGS.copy(),
4904
-                        'backspace\bin\bname': DUMMY_CONFIG_SETTINGS.copy(),
4905
-                        'nul\x00in\x00name': DUMMY_CONFIG_SETTINGS.copy(),
4906
-                        'del\x7fin\x7fname': DUMMY_CONFIG_SETTINGS.copy(),
4907
-                    }
4908
-                },
4909
-                cli_helpers.shell_complete_service,
4910
-                ['vault'],
4911
-                '',
4912
-                sorted([DUMMY_SERVICE, 'colon:in:name']),
4913
-                id='brittle_incompletable_multi_config-service',
4914
-            ),
4915
-            pytest.param(
4916
-                {'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}},
4917
-                cli_helpers.shell_complete_path,
4918
-                ['vault', '--import'],
4919
-                '',
4920
-                [click.shell_completion.CompletionItem('', type='file')],
4921
-                id='base_config-path',
4922
-            ),
4923
-            pytest.param(
4924
-                {'services': {}},
4925
-                cli_helpers.shell_complete_path,
4926
-                ['vault', '--import'],
4927
-                '',
4928
-                [click.shell_completion.CompletionItem('', type='file')],
4929
-                id='empty_config-path',
4930
-            ),
4931
-        ],
4932
-    )
5050
+    @Parametrizations.SHELL_FORMATTER.value
5051
+    @Parametrizations.COMPLETION_FUNCTION_INPUTS.value
4933 5052
     def test_300_shell_completion_formatting(
4934 5053
         self,
4935 5054
         shell: str,
... ...
@@ -4993,156 +5112,8 @@ class TestShellCompletion:
4993 5112
             assert actual_items == expected_items
4994 5113
             assert actual_string == expected_string
4995 5114
 
4996
-    @pytest.mark.parametrize('mode', ['config', 'import'])
4997
-    @pytest.mark.parametrize(
4998
-        ['config', 'key', 'incomplete', 'completions'],
4999
-        [
5000
-            pytest.param(
5001
-                {
5002
-                    'services': {
5003
-                        DUMMY_SERVICE: {'length': 10},
5004
-                        'newline\nin\nname': {'length': 10},
5005
-                    },
5006
-                },
5007
-                'newline\nin\nname',
5008
-                '',
5009
-                frozenset({DUMMY_SERVICE}),
5010
-                id='newline',
5011
-            ),
5012
-            pytest.param(
5013
-                {
5014
-                    'services': {
5015
-                        DUMMY_SERVICE: {'length': 10},
5016
-                        'newline\nin\nname': {'length': 10},
5017
-                    },
5018
-                },
5019
-                'newline\nin\nname',
5020
-                'serv',
5021
-                frozenset({DUMMY_SERVICE}),
5022
-                id='newline_partial_other',
5023
-            ),
5024
-            pytest.param(
5025
-                {
5026
-                    'services': {
5027
-                        DUMMY_SERVICE: {'length': 10},
5028
-                        'newline\nin\nname': {'length': 10},
5029
-                    },
5030
-                },
5031
-                'newline\nin\nname',
5032
-                'newline',
5033
-                frozenset({}),
5034
-                id='newline_partial_specific',
5035
-            ),
5036
-            pytest.param(
5037
-                {
5038
-                    'services': {
5039
-                        DUMMY_SERVICE: {'length': 10},
5040
-                        'nul\x00in\x00name': {'length': 10},
5041
-                    },
5042
-                },
5043
-                'nul\x00in\x00name',
5044
-                '',
5045
-                frozenset({DUMMY_SERVICE}),
5046
-                id='nul',
5047
-            ),
5048
-            pytest.param(
5049
-                {
5050
-                    'services': {
5051
-                        DUMMY_SERVICE: {'length': 10},
5052
-                        'nul\x00in\x00name': {'length': 10},
5053
-                    },
5054
-                },
5055
-                'nul\x00in\x00name',
5056
-                'serv',
5057
-                frozenset({DUMMY_SERVICE}),
5058
-                id='nul_partial_other',
5059
-            ),
5060
-            pytest.param(
5061
-                {
5062
-                    'services': {
5063
-                        DUMMY_SERVICE: {'length': 10},
5064
-                        'nul\x00in\x00name': {'length': 10},
5065
-                    },
5066
-                },
5067
-                'nul\x00in\x00name',
5068
-                'nul',
5069
-                frozenset({}),
5070
-                id='nul_partial_specific',
5071
-            ),
5072
-            pytest.param(
5073
-                {
5074
-                    'services': {
5075
-                        DUMMY_SERVICE: {'length': 10},
5076
-                        'backspace\bin\bname': {'length': 10},
5077
-                    },
5078
-                },
5079
-                'backspace\bin\bname',
5080
-                '',
5081
-                frozenset({DUMMY_SERVICE}),
5082
-                id='backspace',
5083
-            ),
5084
-            pytest.param(
5085
-                {
5086
-                    'services': {
5087
-                        DUMMY_SERVICE: {'length': 10},
5088
-                        'backspace\bin\bname': {'length': 10},
5089
-                    },
5090
-                },
5091
-                'backspace\bin\bname',
5092
-                'serv',
5093
-                frozenset({DUMMY_SERVICE}),
5094
-                id='backspace_partial_other',
5095
-            ),
5096
-            pytest.param(
5097
-                {
5098
-                    'services': {
5099
-                        DUMMY_SERVICE: {'length': 10},
5100
-                        'backspace\bin\bname': {'length': 10},
5101
-                    },
5102
-                },
5103
-                'backspace\bin\bname',
5104
-                'back',
5105
-                frozenset({}),
5106
-                id='backspace_partial_specific',
5107
-            ),
5108
-            pytest.param(
5109
-                {
5110
-                    'services': {
5111
-                        DUMMY_SERVICE: {'length': 10},
5112
-                        'del\x7fin\x7fname': {'length': 10},
5113
-                    },
5114
-                },
5115
-                'del\x7fin\x7fname',
5116
-                '',
5117
-                frozenset({DUMMY_SERVICE}),
5118
-                id='del',
5119
-            ),
5120
-            pytest.param(
5121
-                {
5122
-                    'services': {
5123
-                        DUMMY_SERVICE: {'length': 10},
5124
-                        'del\x7fin\x7fname': {'length': 10},
5125
-                    },
5126
-                },
5127
-                'del\x7fin\x7fname',
5128
-                'serv',
5129
-                frozenset({DUMMY_SERVICE}),
5130
-                id='del_partial_other',
5131
-            ),
5132
-            pytest.param(
5133
-                {
5134
-                    'services': {
5135
-                        DUMMY_SERVICE: {'length': 10},
5136
-                        'del\x7fin\x7fname': {'length': 10},
5137
-                    },
5138
-                },
5139
-                'del\x7fin\x7fname',
5140
-                'del',
5141
-                frozenset({}),
5142
-                id='del_partial_specific',
5143
-            ),
5144
-        ],
5145
-    )
5115
+    @Parametrizations.CONFIG_SETTING_MODE.value
5116
+    @Parametrizations.SERVICE_NAME_COMPLETION_INPUTS.value
5146 5117
     def test_400_incompletable_service_names(
5147 5118
         self,
5148 5119
         caplog: pytest.LogCaptureFixture,
... ...
@@ -5220,7 +5191,7 @@ class TestShellCompletion:
5220 5191
                 '',
5221 5192
             )
5222 5193
 
5223
-    @pytest.mark.parametrize('exc_type', [RuntimeError, KeyError, ValueError])
5194
+    @Parametrizations.SERVICE_NAME_EXCEPTIONS.value
5224 5195
     def test_410b_service_name_exceptions_custom_error(
5225 5196
         self,
5226 5197
         exc_type: type[Exception],
... ...
@@ -6,6 +6,7 @@ from __future__ import annotations
6 6
 
7 7
 import base64
8 8
 import contextlib
9
+import enum
9 10
 import json
10 11
 import pathlib
11 12
 from typing import TYPE_CHECKING
... ...
@@ -39,6 +40,170 @@ if TYPE_CHECKING:
39 40
     from typing_extensions import Buffer, Literal
40 41
 
41 42
 
43
+class Parametrizations(enum.Enum):
44
+    BAD_CONFIG = pytest.mark.parametrize(
45
+        'config', ['xxx', 'null', '{"version": 255}']
46
+    )
47
+    # TODO(the-13th-letter): Rename "result" to "config_data".
48
+    VAULT_NATIVE_CONFIG_DATA = pytest.mark.parametrize(
49
+        ['config', 'format', 'result'],
50
+        [
51
+            pytest.param(
52
+                tests.VAULT_V02_CONFIG,
53
+                'v0.2',
54
+                tests.VAULT_V02_CONFIG_DATA,
55
+                id='V02_CONFIG-v0.2',
56
+            ),
57
+            pytest.param(
58
+                tests.VAULT_V02_CONFIG,
59
+                'v0.3',
60
+                exporter.NotAVaultConfigError,
61
+                id='V02_CONFIG-v0.3',
62
+            ),
63
+            pytest.param(
64
+                tests.VAULT_V03_CONFIG,
65
+                'v0.2',
66
+                exporter.NotAVaultConfigError,
67
+                id='V03_CONFIG-v0.2',
68
+            ),
69
+            pytest.param(
70
+                tests.VAULT_V03_CONFIG,
71
+                'v0.3',
72
+                tests.VAULT_V03_CONFIG_DATA,
73
+                id='V03_CONFIG-v0.3',
74
+            ),
75
+        ],
76
+    )
77
+    BAD_MASTER_KEYS_DATA = pytest.mark.parametrize(
78
+        ['data', 'err_msg'],
79
+        [
80
+            pytest.param(
81
+                '{"version": 255}',
82
+                'bad or unsupported keys version header',
83
+                id='v255',
84
+            ),
85
+            pytest.param(
86
+                '{"version": 1}\nAAAA\nAAAA',
87
+                'trailing data; cannot make sense',
88
+                id='trailing-data',
89
+            ),
90
+            pytest.param(
91
+                '{"version": 1}\nAAAA',
92
+                'cannot handle version 0 encrypted keys',
93
+                id='v0-keys',
94
+            ),
95
+        ],
96
+    )
97
+    # TODO(the-13th-letter): Consolidate with
98
+    # test_derivepassphrase_exporter.Parametrizations.VAULT_CONFIG_FORMATS_DATA.
99
+    # TODO(the-13th-letter): Reorder as "config", "format", "config_data".
100
+    VAULT_CONFIG_FORMATS_DATA = pytest.mark.parametrize(
101
+        ['format', 'config', 'config_data'],
102
+        [
103
+            pytest.param(
104
+                'v0.2',
105
+                tests.VAULT_V02_CONFIG,
106
+                tests.VAULT_V02_CONFIG_DATA,
107
+                id='0.2',
108
+            ),
109
+            pytest.param(
110
+                'v0.3',
111
+                tests.VAULT_V03_CONFIG,
112
+                tests.VAULT_V03_CONFIG_DATA,
113
+                id='0.3',
114
+            ),
115
+            pytest.param(
116
+                'storeroom',
117
+                tests.VAULT_STOREROOM_CONFIG_ZIPPED,
118
+                tests.VAULT_STOREROOM_CONFIG_DATA,
119
+                id='storeroom',
120
+            ),
121
+        ],
122
+    )
123
+    STOREROOM_HANDLER = pytest.mark.parametrize(
124
+        'handler',
125
+        [
126
+            pytest.param(storeroom.export_storeroom_data, id='handler'),
127
+            pytest.param(exporter.export_vault_config_data, id='dispatcher'),
128
+        ],
129
+    )
130
+    VAULT_NATIVE_HANDLER = pytest.mark.parametrize(
131
+        'handler',
132
+        [
133
+            pytest.param(vault_native.export_vault_native_data, id='handler'),
134
+            pytest.param(exporter.export_vault_config_data, id='dispatcher'),
135
+        ],
136
+    )
137
+    VAULT_NATIVE_PBKDF2_RESULT = pytest.mark.parametrize(
138
+        ['iterations', 'result'],
139
+        [
140
+            pytest.param(100, b'6ede361e81e9c061efcdd68aeb768b80', id='100'),
141
+            pytest.param(200, b'bcc7d01e075b9ffb69e702bf701187c1', id='200'),
142
+        ],
143
+    )
144
+    KEY_FORMATS = pytest.mark.parametrize(
145
+        'key',
146
+        [
147
+            None,
148
+            pytest.param(tests.VAULT_MASTER_KEY, id='str'),
149
+            pytest.param(tests.VAULT_MASTER_KEY.encode('ascii'), id='bytes'),
150
+            pytest.param(
151
+                bytearray(tests.VAULT_MASTER_KEY.encode('ascii')),
152
+                id='bytearray',
153
+            ),
154
+            pytest.param(
155
+                memoryview(tests.VAULT_MASTER_KEY.encode('ascii')),
156
+                id='memoryview',
157
+            ),
158
+        ],
159
+    )
160
+    # TODO(the-13th-letter): Reorder and rename to "config", "parser_class",
161
+    # "config_data".
162
+    VAULT_NATIVE_PARSER_CLASS_DATA = pytest.mark.parametrize(
163
+        ['parser_class', 'config', 'result'],
164
+        [
165
+            pytest.param(
166
+                vault_native.VaultNativeV02ConfigParser,
167
+                tests.VAULT_V02_CONFIG,
168
+                tests.VAULT_V02_CONFIG_DATA,
169
+                id='0.2',
170
+            ),
171
+            pytest.param(
172
+                vault_native.VaultNativeV03ConfigParser,
173
+                tests.VAULT_V03_CONFIG,
174
+                tests.VAULT_V03_CONFIG_DATA,
175
+                id='0.3',
176
+            ),
177
+        ],
178
+    )
179
+    PATH = pytest.mark.parametrize('path', ['.vault', None])
180
+    BAD_STOREROOM_CONFIG_DATA = pytest.mark.parametrize(
181
+        ['zipped_config', 'error_text'],
182
+        [
183
+            pytest.param(
184
+                tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED,
185
+                'Object key mismatch',
186
+                id='VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED',
187
+            ),
188
+            pytest.param(
189
+                tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2,
190
+                'Directory index is not actually an index',
191
+                id='VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2',
192
+            ),
193
+            pytest.param(
194
+                tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3,
195
+                'Directory index is not actually an index',
196
+                id='VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3',
197
+            ),
198
+            pytest.param(
199
+                tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4,
200
+                'Object key mismatch',
201
+                id='VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4',
202
+            ),
203
+        ],
204
+    )
205
+
206
+
42 207
 class TestCLI:
43 208
     """Test the command-line interface for `derivepassphrase export vault`."""
44 209
 
... ...
@@ -96,29 +261,7 @@ class TestCLI:
96 261
         assert result.clean_exit(empty_stderr=True), 'expected clean exit'
97 262
         assert json.loads(result.output) == tests.VAULT_V03_CONFIG_DATA
98 263
 
99
-    @pytest.mark.parametrize(
100
-        ['format', 'config', 'config_data'],
101
-        [
102
-            pytest.param(
103
-                'v0.2',
104
-                tests.VAULT_V02_CONFIG,
105
-                tests.VAULT_V02_CONFIG_DATA,
106
-                id='0.2',
107
-            ),
108
-            pytest.param(
109
-                'v0.3',
110
-                tests.VAULT_V03_CONFIG,
111
-                tests.VAULT_V03_CONFIG_DATA,
112
-                id='0.3',
113
-            ),
114
-            pytest.param(
115
-                'storeroom',
116
-                tests.VAULT_STOREROOM_CONFIG_ZIPPED,
117
-                tests.VAULT_STOREROOM_CONFIG_DATA,
118
-                id='storeroom',
119
-            ),
120
-        ],
121
-    )
264
+    @Parametrizations.VAULT_CONFIG_FORMATS_DATA.value
122 265
     def test_210_load_vault_v02_v03_storeroom(
123 266
         self,
124 267
         format: str,
... ...
@@ -324,30 +467,9 @@ class TestCLI:
324 467
 class TestStoreroom:
325 468
     """Test the "storeroom" handler and handler machinery."""
326 469
 
327
-    @pytest.mark.parametrize('path', ['.vault', None])
328
-    @pytest.mark.parametrize(
329
-        'key',
330
-        [
331
-            None,
332
-            pytest.param(tests.VAULT_MASTER_KEY, id='str'),
333
-            pytest.param(tests.VAULT_MASTER_KEY.encode('ascii'), id='bytes'),
334
-            pytest.param(
335
-                bytearray(tests.VAULT_MASTER_KEY.encode('ascii')),
336
-                id='bytearray',
337
-            ),
338
-            pytest.param(
339
-                memoryview(tests.VAULT_MASTER_KEY.encode('ascii')),
340
-                id='memoryview',
341
-            ),
342
-        ],
343
-    )
344
-    @pytest.mark.parametrize(
345
-        'handler',
346
-        [
347
-            pytest.param(storeroom.export_storeroom_data, id='handler'),
348
-            pytest.param(exporter.export_vault_config_data, id='dispatcher'),
349
-        ],
350
-    )
470
+    @Parametrizations.PATH.value
471
+    @Parametrizations.KEY_FORMATS.value
472
+    @Parametrizations.STOREROOM_HANDLER.value
351 473
     def test_200_export_data_path_and_keys_type(
352 474
         self,
353 475
         path: str | None,
... ...
@@ -392,7 +514,7 @@ class TestStoreroom:
392 514
         with pytest.raises(ValueError, match='Cannot handle version 255'):
393 515
             storeroom._decrypt_bucket_item(bucket_item, master_keys)
394 516
 
395
-    @pytest.mark.parametrize('config', ['xxx', 'null', '{"version": 255}'])
517
+    @Parametrizations.BAD_CONFIG.value
396 518
     def test_401_decrypt_bucket_file_bad_json_or_version(
397 519
         self,
398 520
         config: str,
... ...
@@ -427,33 +549,8 @@ class TestStoreroom:
427 549
             with pytest.raises(ValueError, match='Invalid bucket file: '):
428 550
                 list(storeroom._decrypt_bucket_file(p, master_keys))
429 551
 
430
-    @pytest.mark.parametrize(
431
-        ['data', 'err_msg'],
432
-        [
433
-            pytest.param(
434
-                '{"version": 255}',
435
-                'bad or unsupported keys version header',
436
-                id='v255',
437
-            ),
438
-            pytest.param(
439
-                '{"version": 1}\nAAAA\nAAAA',
440
-                'trailing data; cannot make sense',
441
-                id='trailing-data',
442
-            ),
443
-            pytest.param(
444
-                '{"version": 1}\nAAAA',
445
-                'cannot handle version 0 encrypted keys',
446
-                id='v0-keys',
447
-            ),
448
-        ],
449
-    )
450
-    @pytest.mark.parametrize(
451
-        'handler',
452
-        [
453
-            pytest.param(storeroom.export_storeroom_data, id='handler'),
454
-            pytest.param(exporter.export_vault_config_data, id='dispatcher'),
455
-        ],
456
-    )
552
+    @Parametrizations.BAD_MASTER_KEYS_DATA.value
553
+    @Parametrizations.STOREROOM_HANDLER.value
457 554
     def test_402_export_storeroom_data_bad_master_keys_file(
458 555
         self,
459 556
         data: str,
... ...
@@ -485,38 +582,8 @@ class TestStoreroom:
485 582
             with pytest.raises(RuntimeError, match=err_msg):
486 583
                 handler(format='storeroom')
487 584
 
488
-    @pytest.mark.parametrize(
489
-        ['zipped_config', 'error_text'],
490
-        [
491
-            pytest.param(
492
-                tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED,
493
-                'Object key mismatch',
494
-                id='VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED',
495
-            ),
496
-            pytest.param(
497
-                tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2,
498
-                'Directory index is not actually an index',
499
-                id='VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2',
500
-            ),
501
-            pytest.param(
502
-                tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3,
503
-                'Directory index is not actually an index',
504
-                id='VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3',
505
-            ),
506
-            pytest.param(
507
-                tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4,
508
-                'Object key mismatch',
509
-                id='VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4',
510
-            ),
511
-        ],
512
-    )
513
-    @pytest.mark.parametrize(
514
-        'handler',
515
-        [
516
-            pytest.param(storeroom.export_storeroom_data, id='handler'),
517
-            pytest.param(exporter.export_vault_config_data, id='dispatcher'),
518
-        ],
519
-    )
585
+    @Parametrizations.BAD_STOREROOM_CONFIG_DATA.value
586
+    @Parametrizations.STOREROOM_HANDLER.value
520 587
     def test_403_export_storeroom_data_bad_directory_listing(
521 588
         self,
522 589
         zipped_config: bytes,
... ...
@@ -634,13 +701,7 @@ class TestStoreroom:
634 701
 class TestVaultNativeConfig:
635 702
     """Test the vault-native handler and handler machinery."""
636 703
 
637
-    @pytest.mark.parametrize(
638
-        ['iterations', 'result'],
639
-        [
640
-            pytest.param(100, b'6ede361e81e9c061efcdd68aeb768b80', id='100'),
641
-            pytest.param(200, b'bcc7d01e075b9ffb69e702bf701187c1', id='200'),
642
-        ],
643
-    )
704
+    @Parametrizations.VAULT_NATIVE_PBKDF2_RESULT.value
644 705
     def test_200_pbkdf2_manually(self, iterations: int, result: bytes) -> None:
645 706
         """The PBKDF2 helper function works."""
646 707
         assert (
... ...
@@ -650,42 +711,8 @@ class TestVaultNativeConfig:
650 711
             == result
651 712
         )
652 713
 
653
-    @pytest.mark.parametrize(
654
-        ['config', 'format', 'result'],
655
-        [
656
-            pytest.param(
657
-                tests.VAULT_V02_CONFIG,
658
-                'v0.2',
659
-                tests.VAULT_V02_CONFIG_DATA,
660
-                id='V02_CONFIG-v0.2',
661
-            ),
662
-            pytest.param(
663
-                tests.VAULT_V02_CONFIG,
664
-                'v0.3',
665
-                exporter.NotAVaultConfigError,
666
-                id='V02_CONFIG-v0.3',
667
-            ),
668
-            pytest.param(
669
-                tests.VAULT_V03_CONFIG,
670
-                'v0.2',
671
-                exporter.NotAVaultConfigError,
672
-                id='V03_CONFIG-v0.2',
673
-            ),
674
-            pytest.param(
675
-                tests.VAULT_V03_CONFIG,
676
-                'v0.3',
677
-                tests.VAULT_V03_CONFIG_DATA,
678
-                id='V03_CONFIG-v0.3',
679
-            ),
680
-        ],
681
-    )
682
-    @pytest.mark.parametrize(
683
-        'handler',
684
-        [
685
-            pytest.param(vault_native.export_vault_native_data, id='handler'),
686
-            pytest.param(exporter.export_vault_config_data, id='dispatcher'),
687
-        ],
688
-    )
714
+    @Parametrizations.VAULT_NATIVE_CONFIG_DATA.value
715
+    @Parametrizations.VAULT_NATIVE_HANDLER.value
689 716
     def test_201_export_vault_native_data_explicit_version(
690 717
         self,
691 718
         config: str,
... ...
@@ -724,30 +751,9 @@ class TestVaultNativeConfig:
724 751
                 parsed_config = handler(None, format=format)
725 752
                 assert parsed_config == result
726 753
 
727
-    @pytest.mark.parametrize('path', ['.vault', None])
728
-    @pytest.mark.parametrize(
729
-        'key',
730
-        [
731
-            None,
732
-            pytest.param(tests.VAULT_MASTER_KEY, id='str'),
733
-            pytest.param(tests.VAULT_MASTER_KEY.encode('ascii'), id='bytes'),
734
-            pytest.param(
735
-                bytearray(tests.VAULT_MASTER_KEY.encode('ascii')),
736
-                id='bytearray',
737
-            ),
738
-            pytest.param(
739
-                memoryview(tests.VAULT_MASTER_KEY.encode('ascii')),
740
-                id='memoryview',
741
-            ),
742
-        ],
743
-    )
744
-    @pytest.mark.parametrize(
745
-        'handler',
746
-        [
747
-            pytest.param(vault_native.export_vault_native_data, id='handler'),
748
-            pytest.param(exporter.export_vault_config_data, id='dispatcher'),
749
-        ],
750
-    )
754
+    @Parametrizations.PATH.value
755
+    @Parametrizations.KEY_FORMATS.value
756
+    @Parametrizations.VAULT_NATIVE_HANDLER.value
751 757
     def test_202_export_data_path_and_keys_type(
752 758
         self,
753 759
         path: str | None,
... ...
@@ -779,23 +785,7 @@ class TestVaultNativeConfig:
779 785
                 == tests.VAULT_V03_CONFIG_DATA
780 786
             )
781 787
 
782
-    @pytest.mark.parametrize(
783
-        ['parser_class', 'config', 'result'],
784
-        [
785
-            pytest.param(
786
-                vault_native.VaultNativeV02ConfigParser,
787
-                tests.VAULT_V02_CONFIG,
788
-                tests.VAULT_V02_CONFIG_DATA,
789
-                id='0.2',
790
-            ),
791
-            pytest.param(
792
-                vault_native.VaultNativeV03ConfigParser,
793
-                tests.VAULT_V03_CONFIG,
794
-                tests.VAULT_V03_CONFIG_DATA,
795
-                id='0.3',
796
-            ),
797
-        ],
798
-    )
788
+    @Parametrizations.VAULT_NATIVE_PARSER_CLASS_DATA.value
799 789
     def test_300_result_caching(
800 790
         self,
801 791
         parser_class: type[vault_native.VaultNativeConfigParser],
... ...
@@ -5,6 +5,7 @@
5 5
 from __future__ import annotations
6 6
 
7 7
 import contextlib
8
+import enum
8 9
 import operator
9 10
 import os
10 11
 import pathlib
... ...
@@ -23,6 +24,59 @@ if TYPE_CHECKING:
23 24
     from typing_extensions import Buffer
24 25
 
25 26
 
27
+class Parametrizations(enum.Enum):
28
+    EXPECTED_VAULT_PATH = pytest.mark.parametrize(
29
+        ['expected', 'path'],
30
+        [
31
+            (pathlib.Path('/tmp'), pathlib.Path('/tmp')),
32
+            (pathlib.Path('~'), pathlib.Path()),
33
+            (pathlib.Path('~/.vault'), None),
34
+        ],
35
+    )
36
+    # TODO(the-13th-letter): Consolidate with
37
+    # test_derivepassphrase_cli_export_vault.Parametrizations.VAULT_CONFIG_FORMATS_DATA.
38
+    # TODO(the-13th-letter): Reorder as "config", "format", "config_data".
39
+    VAULT_CONFIG_FORMATS_DATA = pytest.mark.parametrize(
40
+        ['format', 'config', 'config_data'],
41
+        [
42
+            pytest.param(
43
+                'v0.2',
44
+                tests.VAULT_V02_CONFIG,
45
+                tests.VAULT_V02_CONFIG_DATA,
46
+                id='0.2',
47
+            ),
48
+            pytest.param(
49
+                'v0.3',
50
+                tests.VAULT_V03_CONFIG,
51
+                tests.VAULT_V03_CONFIG_DATA,
52
+                id='0.3',
53
+            ),
54
+            pytest.param(
55
+                'storeroom',
56
+                tests.VAULT_STOREROOM_CONFIG_ZIPPED,
57
+                tests.VAULT_STOREROOM_CONFIG_DATA,
58
+                id='storeroom',
59
+            ),
60
+        ],
61
+    )
62
+    EXPORT_VAULT_CONFIG_DATA_HANDLER_NAMELISTS = pytest.mark.parametrize(
63
+        ['namelist', 'err_pat'],
64
+        [
65
+            pytest.param((), '[Nn]o names given', id='empty'),
66
+            pytest.param(
67
+                ('name1', '', 'name2'),
68
+                '[Uu]nder an empty name',
69
+                id='empty-string',
70
+            ),
71
+            pytest.param(
72
+                ('dummy', 'name1', 'name2'),
73
+                '[Aa]lready registered',
74
+                id='existing',
75
+            ),
76
+        ],
77
+    )
78
+
79
+
26 80
 class Test001ExporterUtils:
27 81
     """Test the utility functions in the `exporter` subpackage."""
28 82
 
... ...
@@ -194,14 +248,7 @@ class Test001ExporterUtils:
194 248
                     monkeypatch.setenv(key, value)
195 249
             assert os.fsdecode(exporter.get_vault_key()) == expected
196 250
 
197
-    @pytest.mark.parametrize(
198
-        ['expected', 'path'],
199
-        [
200
-            (pathlib.Path('/tmp'), pathlib.Path('/tmp')),
201
-            (pathlib.Path('~'), pathlib.Path()),
202
-            (pathlib.Path('~/.vault'), None),
203
-        ],
204
-    )
251
+    @Parametrizations.EXPECTED_VAULT_PATH.value
205 252
     def test_210_get_vault_path(
206 253
         self,
207 254
         expected: pathlib.Path,
... ...
@@ -304,22 +351,7 @@ class Test001ExporterUtils:
304 351
             ):
305 352
                 exporter.get_vault_path()
306 353
 
307
-    @pytest.mark.parametrize(
308
-        ['namelist', 'err_pat'],
309
-        [
310
-            pytest.param((), '[Nn]o names given', id='empty'),
311
-            pytest.param(
312
-                ('name1', '', 'name2'),
313
-                '[Uu]nder an empty name',
314
-                id='empty-string',
315
-            ),
316
-            pytest.param(
317
-                ('dummy', 'name1', 'name2'),
318
-                '[Aa]lready registered',
319
-                id='existing',
320
-            ),
321
-        ],
322
-    )
354
+    @Parametrizations.EXPORT_VAULT_CONFIG_DATA_HANDLER_NAMELISTS.value
323 355
     def test_320_register_export_vault_config_data_handler_errors(
324 356
         self,
325 357
         namelist: tuple[str, ...],
... ...
@@ -398,29 +430,7 @@ class Test002CLI:
398 430
             )
399 431
 
400 432
     @tests.skip_if_cryptography_support
401
-    @pytest.mark.parametrize(
402
-        ['format', 'config', 'key'],
403
-        [
404
-            pytest.param(
405
-                'v0.2',
406
-                tests.VAULT_V02_CONFIG,
407
-                tests.VAULT_MASTER_KEY,
408
-                id='v0.2',
409
-            ),
410
-            pytest.param(
411
-                'v0.3',
412
-                tests.VAULT_V03_CONFIG,
413
-                tests.VAULT_MASTER_KEY,
414
-                id='v0.3',
415
-            ),
416
-            pytest.param(
417
-                'storeroom',
418
-                tests.VAULT_STOREROOM_CONFIG_ZIPPED,
419
-                tests.VAULT_MASTER_KEY,
420
-                id='storeroom',
421
-            ),
422
-        ],
423
-    )
433
+    @Parametrizations.VAULT_CONFIG_FORMATS_DATA.value
424 434
     def test_999_no_cryptography_error_message(
425 435
         self,
426 436
         caplog: pytest.LogCaptureFixture,
... ...
@@ -8,6 +8,7 @@ from __future__ import annotations
8 8
 
9 9
 import collections
10 10
 import contextlib
11
+import enum
11 12
 import functools
12 13
 import math
13 14
 import operator
... ...
@@ -54,6 +55,29 @@ def bitseq(string: str) -> list[int]:
54 55
     return [int(char, 2) for char in string]
55 56
 
56 57
 
58
+class Parametrizations(enum.Enum):
59
+    BIG_ENDIAN_NUMBER_EXCEPTIONS = pytest.mark.parametrize(
60
+        ['exc_type', 'exc_pattern', 'sequence', 'base'],
61
+        [
62
+            (ValueError, 'invalid base 3 digit:', [-1], 3),
63
+            (ValueError, 'invalid base:', [0], 1),
64
+            (TypeError, 'not an integer:', [0.0, 1.0, 0.0, 1.0], 2),
65
+        ],
66
+    )
67
+    INVALID_SEQUIN_INPUTS = pytest.mark.parametrize(
68
+        ['sequence', 'is_bitstring', 'exc_type', 'exc_pattern'],
69
+        [
70
+            (
71
+                [0, 1, 2, 3, 4, 5, 6, 7],
72
+                True,
73
+                ValueError,
74
+                'sequence item out of range',
75
+            ),
76
+            ('こんにちは。', False, ValueError, 'sequence item out of range'),
77
+        ],
78
+    )
79
+
80
+
57 81
 class TestStaticFunctionality:
58 82
     """Test the static functionality in the `sequin` module."""
59 83
 
... ...
@@ -175,14 +199,7 @@ class TestStaticFunctionality:
175 199
             sequin.Sequin._big_endian_number(sequence, base=base)
176 200
         ) == expected
177 201
 
178
-    @pytest.mark.parametrize(
179
-        ['exc_type', 'exc_pattern', 'sequence', 'base'],
180
-        [
181
-            (ValueError, 'invalid base 3 digit:', [-1], 3),
182
-            (ValueError, 'invalid base:', [0], 1),
183
-            (TypeError, 'not an integer:', [0.0, 1.0, 0.0, 1.0], 2),
184
-        ],
185
-    )
202
+    @Parametrizations.BIG_ENDIAN_NUMBER_EXCEPTIONS.value
186 203
     def test_300_big_endian_number_exceptions(
187 204
         self,
188 205
         exc_type: type[Exception],
... ...
@@ -524,18 +541,7 @@ class TestSequin:
524 541
                     f'After step {i}, the bit sequence is not exhausted yet'
525 542
                 )
526 543
 
527
-    @pytest.mark.parametrize(
528
-        ['sequence', 'is_bitstring', 'exc_type', 'exc_pattern'],
529
-        [
530
-            (
531
-                [0, 1, 2, 3, 4, 5, 6, 7],
532
-                True,
533
-                ValueError,
534
-                'sequence item out of range',
535
-            ),
536
-            ('こんにちは。', False, ValueError, 'sequence item out of range'),
537
-        ],
538
-    )
544
+    @Parametrizations.INVALID_SEQUIN_INPUTS.value
539 545
     def test_300_constructor_exceptions(
540 546
         self,
541 547
         sequence: list[int] | str,
... ...
@@ -8,6 +8,7 @@ from __future__ import annotations
8 8
 
9 9
 import base64
10 10
 import contextlib
11
+import enum
11 12
 import io
12 13
 import re
13 14
 import socket
... ...
@@ -29,74 +30,125 @@ if TYPE_CHECKING:
29 30
     from typing_extensions import Any, Buffer
30 31
 
31 32
 
32
-class TestStaticFunctionality:
33
-    """Test the static functionality of the `ssh_agent` module."""
34
-
35
-    @staticmethod
36
-    def as_ssh_string(bytestring: bytes) -> bytes:
37
-        """Return an encoded SSH string from a bytestring.
38
-
39
-        This is a helper function for hypothesis data generation.
40
-
41
-        """
42
-        return int.to_bytes(len(bytestring), 4, 'big') + bytestring
43
-
44
-    @staticmethod
45
-    def canonicalize1(data: bytes) -> bytes:
46
-        """Return an encoded SSH string from a bytestring.
47
-
48
-        This is a helper function for hypothesis testing.
49
-
50
-        References:
51
-
52
-          * [David R. MacIver: Another invariant to test for
53
-            encoders][DECODE_ENCODE]
54
-
55
-        [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/
56
-
57
-        """
58
-        return ssh_agent.SSHAgentClient.string(
59
-            ssh_agent.SSHAgentClient.unstring(data)
33
+class Parametrizations(enum.Enum):
34
+    SSH_STRING_EXCEPTIONS = pytest.mark.parametrize(
35
+        ['input', 'exc_type', 'exc_pattern'],
36
+        [
37
+            pytest.param(
38
+                'some string', TypeError, 'invalid payload type', id='str'
39
+            ),
40
+        ],
60 41
     )
61
-
62
-    @staticmethod
63
-    def canonicalize2(data: bytes) -> bytes:
64
-        """Return an encoded SSH string from a bytestring.
65
-
66
-        This is a helper function for hypothesis testing.
67
-
68
-        References:
69
-
70
-          * [David R. MacIver: Another invariant to test for
71
-            encoders][DECODE_ENCODE]
72
-
73
-        [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/
74
-
75
-        """
76
-        unstringed, trailer = ssh_agent.SSHAgentClient.unstring_prefix(data)
77
-        assert not trailer
78
-        return ssh_agent.SSHAgentClient.string(unstringed)
79
-
80
-    # TODO(the-13th-letter): Re-evaluate if this check is worth keeping.
81
-    # It cannot provide true tamper-resistence, but probably appears to.
82
-    @pytest.mark.parametrize(
83
-        ['public_key', 'public_key_data'],
42
+    SSH_UNSTRING_EXCEPTIONS = pytest.mark.parametrize(
43
+        ['input', 'exc_type', 'exc_pattern', 'has_trailer', 'parts'],
84 44
         [
85
-            (val.public_key, val.public_key_data)
86
-            for val in tests.SUPPORTED_KEYS.values()
45
+            pytest.param(
46
+                b'ssh',
47
+                ValueError,
48
+                'malformed SSH byte string',
49
+                False,
50
+                None,
51
+                id='unencoded',
52
+            ),
53
+            pytest.param(
54
+                b'\x00\x00\x00\x08ssh-rsa',
55
+                ValueError,
56
+                'malformed SSH byte string',
57
+                False,
58
+                None,
59
+                id='truncated',
60
+            ),
61
+            pytest.param(
62
+                b'\x00\x00\x00\x04XXX trailing text',
63
+                ValueError,
64
+                'malformed SSH byte string',
65
+                True,
66
+                (b'XXX ', b'trailing text'),
67
+                id='trailing-data',
68
+            ),
87 69
         ],
88
-        ids=list(tests.SUPPORTED_KEYS.keys()),
89 70
     )
90
-    def test_100_key_decoding(
91
-        self, public_key: bytes, public_key_data: bytes
92
-    ) -> None:
93
-        """The [`tests.ALL_KEYS`][] public key data looks sane."""
94
-        keydata = base64.b64decode(public_key.split(None, 2)[1])
95
-        assert keydata == public_key_data, (
96
-            "recorded public key data doesn't match"
71
+    SSH_STRING_INPUT = pytest.mark.parametrize(
72
+        ['input', 'expected'],
73
+        [
74
+            pytest.param(
75
+                b'ssh-rsa',
76
+                b'\x00\x00\x00\x07ssh-rsa',
77
+                id='ssh-rsa',
78
+            ),
79
+            pytest.param(
80
+                b'ssh-ed25519',
81
+                b'\x00\x00\x00\x0bssh-ed25519',
82
+                id='ssh-ed25519',
83
+            ),
84
+            pytest.param(
85
+                ssh_agent.SSHAgentClient.string(b'ssh-ed25519'),
86
+                b'\x00\x00\x00\x0f\x00\x00\x00\x0bssh-ed25519',
87
+                id='string(ssh-ed25519)',
88
+            ),
89
+        ],
97 90
     )
98
-
99
-    @pytest.mark.parametrize(
91
+    SSH_UNSTRING_INPUT = pytest.mark.parametrize(
92
+        ['input', 'expected'],
93
+        [
94
+            pytest.param(
95
+                b'\x00\x00\x00\x07ssh-rsa',
96
+                b'ssh-rsa',
97
+                id='ssh-rsa',
98
+            ),
99
+            pytest.param(
100
+                ssh_agent.SSHAgentClient.string(b'ssh-ed25519'),
101
+                b'ssh-ed25519',
102
+                id='ssh-ed25519',
103
+            ),
104
+        ],
105
+    )
106
+    UINT32_INPUT = pytest.mark.parametrize(
107
+        ['input', 'expected'],
108
+        [
109
+            pytest.param(16777216, b'\x01\x00\x00\x00', id='16777216'),
110
+        ],
111
+    )
112
+    SIGN_ERROR_RESPONSES = pytest.mark.parametrize(
113
+        [
114
+            'key',
115
+            'check',
116
+            'response_code',
117
+            'response',
118
+            'exc_type',
119
+            'exc_pattern',
120
+        ],
121
+        [
122
+            pytest.param(
123
+                b'invalid-key',
124
+                True,
125
+                _types.SSH_AGENT.FAILURE,
126
+                b'',
127
+                KeyError,
128
+                'target SSH key not loaded into agent',
129
+                id='key-not-loaded',
130
+            ),
131
+            pytest.param(
132
+                tests.SUPPORTED_KEYS['ed25519'].public_key_data,
133
+                True,
134
+                _types.SSH_AGENT.FAILURE,
135
+                b'',
136
+                ssh_agent.SSHAgentFailedError,
137
+                'failed to complete the request',
138
+                id='failed-to-complete',
139
+            ),
140
+        ],
141
+    )
142
+    SSH_KEY_SELECTION = pytest.mark.parametrize(
143
+        ['key', 'single'],
144
+        [
145
+            (value.public_key_data, False)
146
+            for value in tests.SUPPORTED_KEYS.values()
147
+        ]
148
+        + [(tests.list_keys_singleton()[0].key, True)],
149
+        ids=[*tests.SUPPORTED_KEYS.keys(), 'singleton'],
150
+    )
151
+    SH_EXPORT_LINES = pytest.mark.parametrize(
100 152
         ['line', 'env_name', 'value'],
101 153
         [
102 154
             pytest.param(
... ...
@@ -167,6 +219,175 @@ class TestStaticFunctionality:
167 219
             ),
168 220
         ],
169 221
     )
222
+    # TODO(the-13th-letter): Modify receiver to receive the whole struct
223
+    # directly.
224
+    PUBLIC_KEY_DATA = pytest.mark.parametrize(
225
+        ['public_key', 'public_key_data'],
226
+        [
227
+            (val.public_key, val.public_key_data)
228
+            for val in tests.SUPPORTED_KEYS.values()
229
+        ],
230
+        ids=list(tests.SUPPORTED_KEYS.keys()),
231
+    )
232
+    REQUEST_ERROR_RESPONSES = pytest.mark.parametrize(
233
+        ['request_code', 'response_code', 'exc_type', 'exc_pattern'],
234
+        [
235
+            pytest.param(
236
+                _types.SSH_AGENTC.REQUEST_IDENTITIES,
237
+                _types.SSH_AGENT.SUCCESS,
238
+                ssh_agent.SSHAgentFailedError,
239
+                re.escape(
240
+                    f'[Code {_types.SSH_AGENT.IDENTITIES_ANSWER.value}]'
241
+                ),
242
+                id='REQUEST_IDENTITIES-expect-SUCCESS',
243
+            ),
244
+        ],
245
+    )
246
+    TRUNCATED_AGENT_RESPONSES = pytest.mark.parametrize(
247
+        'response',
248
+        [
249
+            b'\x00\x00',
250
+            b'\x00\x00\x00\x1f some bytes missing',
251
+        ],
252
+        ids=['in-header', 'in-body'],
253
+    )
254
+    LIST_KEYS_ERROR_RESPONSES = pytest.mark.parametrize(
255
+        ['response_code', 'response', 'exc_type', 'exc_pattern'],
256
+        [
257
+            pytest.param(
258
+                _types.SSH_AGENT.FAILURE,
259
+                b'',
260
+                ssh_agent.SSHAgentFailedError,
261
+                'failed to complete the request',
262
+                id='failed-to-complete',
263
+            ),
264
+            pytest.param(
265
+                _types.SSH_AGENT.IDENTITIES_ANSWER,
266
+                b'\x00\x00\x00\x01',
267
+                EOFError,
268
+                'truncated response',
269
+                id='truncated-response',
270
+            ),
271
+            pytest.param(
272
+                _types.SSH_AGENT.IDENTITIES_ANSWER,
273
+                b'\x00\x00\x00\x00abc',
274
+                ssh_agent.TrailingDataError,
275
+                'Overlong response',
276
+                id='overlong-response',
277
+            ),
278
+        ],
279
+    )
280
+    QUERY_EXTENSIONS_MALFORMED_RESPONSES = pytest.mark.parametrize(
281
+        'response_data',
282
+        [
283
+            pytest.param(b'\xde\xad\xbe\xef', id='truncated'),
284
+            pytest.param(
285
+                b'\x00\x00\x00\x0fwrong extension', id='wrong-extension'
286
+            ),
287
+            pytest.param(
288
+                b'\x00\x00\x00\x05query\xde\xad\xbe\xef', id='with-trailer'
289
+            ),
290
+            pytest.param(
291
+                b'\x00\x00\x00\x05query\x00\x00\x00\x04ext1\x00\x00',
292
+                id='with-extra-fields',
293
+            ),
294
+        ],
295
+    )
296
+    # TODO(the-13th-letter): Also yield the key type, for reporting purposes.
297
+    SUPPORTED_SSH_TEST_KEYS = pytest.mark.parametrize(
298
+        'ssh_test_key',
299
+        list(tests.SUPPORTED_KEYS.values()),
300
+        ids=tests.SUPPORTED_KEYS.keys(),
301
+    )
302
+    # TODO(the-13th-letter): Also yield the key type, for reporting purposes.
303
+    UNSUITABLE_SSH_TEST_KEYS = pytest.mark.parametrize(
304
+        'ssh_test_key',
305
+        list(tests.UNSUITABLE_KEYS.values()),
306
+        ids=tests.UNSUITABLE_KEYS.keys(),
307
+    )
308
+    # TODO(the-13th-letter): Rename "value" to "input".
309
+    UINT32_EXCEPTIONS = pytest.mark.parametrize(
310
+        ['value', 'exc_type', 'exc_pattern'],
311
+        [
312
+            pytest.param(
313
+                10000000000000000,
314
+                OverflowError,
315
+                'int too big to convert',
316
+                id='10000000000000000',
317
+            ),
318
+            pytest.param(
319
+                -1,
320
+                OverflowError,
321
+                "can't convert negative int to unsigned",
322
+                id='-1',
323
+            ),
324
+        ],
325
+    )
326
+
327
+
328
+class TestStaticFunctionality:
329
+    """Test the static functionality of the `ssh_agent` module."""
330
+
331
+    @staticmethod
332
+    def as_ssh_string(bytestring: bytes) -> bytes:
333
+        """Return an encoded SSH string from a bytestring.
334
+
335
+        This is a helper function for hypothesis data generation.
336
+
337
+        """
338
+        return int.to_bytes(len(bytestring), 4, 'big') + bytestring
339
+
340
+    @staticmethod
341
+    def canonicalize1(data: bytes) -> bytes:
342
+        """Return an encoded SSH string from a bytestring.
343
+
344
+        This is a helper function for hypothesis testing.
345
+
346
+        References:
347
+
348
+          * [David R. MacIver: Another invariant to test for
349
+            encoders][DECODE_ENCODE]
350
+
351
+        [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/
352
+
353
+        """
354
+        return ssh_agent.SSHAgentClient.string(
355
+            ssh_agent.SSHAgentClient.unstring(data)
356
+        )
357
+
358
+    @staticmethod
359
+    def canonicalize2(data: bytes) -> bytes:
360
+        """Return an encoded SSH string from a bytestring.
361
+
362
+        This is a helper function for hypothesis testing.
363
+
364
+        References:
365
+
366
+          * [David R. MacIver: Another invariant to test for
367
+            encoders][DECODE_ENCODE]
368
+
369
+        [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/
370
+
371
+        """
372
+        unstringed, trailer = ssh_agent.SSHAgentClient.unstring_prefix(data)
373
+        assert not trailer
374
+        return ssh_agent.SSHAgentClient.string(unstringed)
375
+
376
+    # TODO(the-13th-letter): Re-evaluate if this check is worth keeping.
377
+    # It cannot provide true tamper-resistence, but probably appears to.
378
+    # TODO(the-13th-letter): Modify parametrization to work directly on the
379
+    # struct.
380
+    @Parametrizations.PUBLIC_KEY_DATA.value
381
+    def test_100_key_decoding(
382
+        self, public_key: bytes, public_key_data: bytes
383
+    ) -> None:
384
+        """The [`tests.ALL_KEYS`][] public key data looks sane."""
385
+        keydata = base64.b64decode(public_key.split(None, 2)[1])
386
+        assert keydata == public_key_data, (
387
+            "recorded public key data doesn't match"
388
+        )
389
+
390
+    @Parametrizations.SH_EXPORT_LINES.value
170 391
     def test_190_sh_export_line_parsing(
171 392
         self, line: str, env_name: str, value: str | None
172 393
     ) -> None:
... ...
@@ -190,12 +411,7 @@ class TestStaticFunctionality:
190 411
             ):
191 412
                 ssh_agent.SSHAgentClient()
192 413
 
193
-    @pytest.mark.parametrize(
194
-        ['input', 'expected'],
195
-        [
196
-            pytest.param(16777216, b'\x01\x00\x00\x00', id='16777216'),
197
-        ],
198
-    )
414
+    @Parametrizations.UINT32_INPUT.value
199 415
     def test_210_uint32(self, input: int, expected: bytes | bytearray) -> None:
200 416
         """`uint32` encoding works."""
201 417
         uint32 = ssh_agent.SSHAgentClient.uint32
... ...
@@ -220,26 +436,7 @@ class TestStaticFunctionality:
220 436
             == bytestring
221 437
         )
222 438
 
223
-    @pytest.mark.parametrize(
224
-        ['input', 'expected'],
225
-        [
226
-            pytest.param(
227
-                b'ssh-rsa',
228
-                b'\x00\x00\x00\x07ssh-rsa',
229
-                id='ssh-rsa',
230
-            ),
231
-            pytest.param(
232
-                b'ssh-ed25519',
233
-                b'\x00\x00\x00\x0bssh-ed25519',
234
-                id='ssh-ed25519',
235
-            ),
236
-            pytest.param(
237
-                ssh_agent.SSHAgentClient.string(b'ssh-ed25519'),
238
-                b'\x00\x00\x00\x0f\x00\x00\x00\x0bssh-ed25519',
239
-                id='string(ssh-ed25519)',
240
-            ),
241
-        ],
242
-    )
439
+    @Parametrizations.SSH_STRING_INPUT.value
243 440
     def test_211_string(
244 441
         self, input: bytes | bytearray, expected: bytes | bytearray
245 442
     ) -> None:
... ...
@@ -258,21 +455,7 @@ class TestStaticFunctionality:
258 455
         assert int.from_bytes(res[:4], 'big', signed=False) == len(bytestring)
259 456
         assert res[4:] == bytestring
260 457
 
261
-    @pytest.mark.parametrize(
262
-        ['input', 'expected'],
263
-        [
264
-            pytest.param(
265
-                b'\x00\x00\x00\x07ssh-rsa',
266
-                b'ssh-rsa',
267
-                id='ssh-rsa',
268
-            ),
269
-            pytest.param(
270
-                ssh_agent.SSHAgentClient.string(b'ssh-ed25519'),
271
-                b'ssh-ed25519',
272
-                id='ssh-ed25519',
273
-            ),
274
-        ],
275
-    )
458
+    @Parametrizations.SSH_UNSTRING_INPUT.value
276 459
     def test_212_unstring(
277 460
         self, input: bytes | bytearray, expected: bytes | bytearray
278 461
     ) -> None:
... ...
@@ -337,23 +520,8 @@ class TestStaticFunctionality:
337 520
                 assert canon1(encoded) == canon2(encoded)
338 521
                 assert canon1(canon2(encoded)) == canon1(encoded)
339 522
 
340
-    @pytest.mark.parametrize(
341
-        ['value', 'exc_type', 'exc_pattern'],
342
-        [
343
-            pytest.param(
344
-                10000000000000000,
345
-                OverflowError,
346
-                'int too big to convert',
347
-                id='10000000000000000',
348
-            ),
349
-            pytest.param(
350
-                -1,
351
-                OverflowError,
352
-                "can't convert negative int to unsigned",
353
-                id='-1',
354
-            ),
355
-        ],
356
-    )
523
+    # TODO(the-13th-letter): Rename "value" to "input".
524
+    @Parametrizations.UINT32_EXCEPTIONS.value
357 525
     def test_310_uint32_exceptions(
358 526
         self, value: int, exc_type: type[Exception], exc_pattern: str
359 527
     ) -> None:
... ...
@@ -362,14 +530,7 @@ class TestStaticFunctionality:
362 530
         with pytest.raises(exc_type, match=exc_pattern):
363 531
             uint32(value)
364 532
 
365
-    @pytest.mark.parametrize(
366
-        ['input', 'exc_type', 'exc_pattern'],
367
-        [
368
-            pytest.param(
369
-                'some string', TypeError, 'invalid payload type', id='str'
370
-            ),
371
-        ],
372
-    )
533
+    @Parametrizations.SSH_STRING_EXCEPTIONS.value
373 534
     def test_311_string_exceptions(
374 535
         self, input: Any, exc_type: type[Exception], exc_pattern: str
375 536
     ) -> None:
... ...
@@ -378,35 +539,7 @@ class TestStaticFunctionality:
378 539
         with pytest.raises(exc_type, match=exc_pattern):
379 540
             string(input)
380 541
 
381
-    @pytest.mark.parametrize(
382
-        ['input', 'exc_type', 'exc_pattern', 'has_trailer', 'parts'],
383
-        [
384
-            pytest.param(
385
-                b'ssh',
386
-                ValueError,
387
-                'malformed SSH byte string',
388
-                False,
389
-                None,
390
-                id='unencoded',
391
-            ),
392
-            pytest.param(
393
-                b'\x00\x00\x00\x08ssh-rsa',
394
-                ValueError,
395
-                'malformed SSH byte string',
396
-                False,
397
-                None,
398
-                id='truncated',
399
-            ),
400
-            pytest.param(
401
-                b'\x00\x00\x00\x04XXX trailing text',
402
-                ValueError,
403
-                'malformed SSH byte string',
404
-                True,
405
-                (b'XXX ', b'trailing text'),
406
-                id='trailing-data',
407
-            ),
408
-        ],
409
-    )
542
+    @Parametrizations.SSH_UNSTRING_EXCEPTIONS.value
410 543
     def test_312_unstring_exceptions(
411 544
         self,
412 545
         input: bytes | bytearray,
... ...
@@ -433,11 +566,7 @@ class TestAgentInteraction:
433 566
     # TODO(the-13th-letter): Convert skip into xfail, and include the
434 567
     # key type in the skip/xfail message.  This means the key type needs
435 568
     # to be passed to the test function as well.
436
-    @pytest.mark.parametrize(
437
-        'ssh_test_key',
438
-        list(tests.SUPPORTED_KEYS.values()),
439
-        ids=tests.SUPPORTED_KEYS.keys(),
440
-    )
569
+    @Parametrizations.SUPPORTED_SSH_TEST_KEYS.value
441 570
     def test_200_sign_data_via_agent(
442 571
         self,
443 572
         ssh_agent_client_with_test_keys_loaded: ssh_agent.SSHAgentClient,
... ...
@@ -475,11 +604,7 @@ class TestAgentInteraction:
475 604
     # TODO(the-13th-letter): Include the key type in the skip message.
476 605
     # This means the key type needs to be passed to the test function as
477 606
     # well.
478
-    @pytest.mark.parametrize(
479
-        'ssh_test_key',
480
-        list(tests.UNSUITABLE_KEYS.values()),
481
-        ids=tests.UNSUITABLE_KEYS.keys(),
482
-    )
607
+    @Parametrizations.UNSUITABLE_SSH_TEST_KEYS.value
483 608
     def test_201_sign_data_via_agent_unsupported(
484 609
         self,
485 610
         ssh_agent_client_with_test_keys_loaded: ssh_agent.SSHAgentClient,
... ...
@@ -507,15 +632,7 @@ class TestAgentInteraction:
507 632
         with pytest.raises(ValueError, match='unsuitable SSH key'):
508 633
             vault.Vault.phrase_from_key(public_key_data, conn=client)
509 634
 
510
-    @pytest.mark.parametrize(
511
-        ['key', 'single'],
512
-        [
513
-            (value.public_key_data, False)
514
-            for value in tests.SUPPORTED_KEYS.values()
515
-        ]
516
-        + [(tests.list_keys_singleton()[0].key, True)],
517
-        ids=[*tests.SUPPORTED_KEYS.keys(), 'singleton'],
518
-    )
635
+    @Parametrizations.SSH_KEY_SELECTION.value
519 636
     def test_210_ssh_key_selector(
520 637
         self,
521 638
         monkeypatch: pytest.MonkeyPatch,
... ...
@@ -616,14 +733,7 @@ class TestAgentInteraction:
616 733
             ):
617 734
                 ssh_agent.SSHAgentClient()
618 735
 
619
-    @pytest.mark.parametrize(
620
-        'response',
621
-        [
622
-            b'\x00\x00',
623
-            b'\x00\x00\x00\x1f some bytes missing',
624
-        ],
625
-        ids=['in-header', 'in-body'],
626
-    )
736
+    @Parametrizations.TRUNCATED_AGENT_RESPONSES.value
627 737
     def test_310_truncated_server_response(
628 738
         self,
629 739
         running_ssh_agent: tests.RunningSSHAgentInfo,
... ...
@@ -647,32 +757,7 @@ class TestAgentInteraction:
647 757
             with pytest.raises(EOFError):
648 758
                 client.request(255, b'')
649 759
 
650
-    @pytest.mark.parametrize(
651
-        ['response_code', 'response', 'exc_type', 'exc_pattern'],
652
-        [
653
-            pytest.param(
654
-                _types.SSH_AGENT.FAILURE,
655
-                b'',
656
-                ssh_agent.SSHAgentFailedError,
657
-                'failed to complete the request',
658
-                id='failed-to-complete',
659
-            ),
660
-            pytest.param(
661
-                _types.SSH_AGENT.IDENTITIES_ANSWER,
662
-                b'\x00\x00\x00\x01',
663
-                EOFError,
664
-                'truncated response',
665
-                id='truncated-response',
666
-            ),
667
-            pytest.param(
668
-                _types.SSH_AGENT.IDENTITIES_ANSWER,
669
-                b'\x00\x00\x00\x00abc',
670
-                ssh_agent.TrailingDataError,
671
-                'Overlong response',
672
-                id='overlong-response',
673
-            ),
674
-        ],
675
-    )
760
+    @Parametrizations.LIST_KEYS_ERROR_RESPONSES.value
676 761
     def test_320_list_keys_error_responses(
677 762
         self,
678 763
         running_ssh_agent: tests.RunningSSHAgentInfo,
... ...
@@ -695,6 +780,8 @@ class TestAgentInteraction:
695 780
 
696 781
         passed_response_code = response_code
697 782
 
783
+        # TODO(the-13th-letter): Extract this mock function into a common
784
+        # top-level "request" mock function.
698 785
         def request(
699 786
             request_code: int | _types.SSH_AGENTC,
700 787
             payload: bytes | bytearray,
... ...
@@ -730,36 +817,7 @@ class TestAgentInteraction:
730 817
             with pytest.raises(exc_type, match=exc_pattern):
731 818
                 client.list_keys()
732 819
 
733
-    @pytest.mark.parametrize(
734
-        [
735
-            'key',
736
-            'check',
737
-            'response_code',
738
-            'response',
739
-            'exc_type',
740
-            'exc_pattern',
741
-        ],
742
-        [
743
-            pytest.param(
744
-                b'invalid-key',
745
-                True,
746
-                _types.SSH_AGENT.FAILURE,
747
-                b'',
748
-                KeyError,
749
-                'target SSH key not loaded into agent',
750
-                id='key-not-loaded',
751
-            ),
752
-            pytest.param(
753
-                tests.SUPPORTED_KEYS['ed25519'].public_key_data,
754
-                True,
755
-                _types.SSH_AGENT.FAILURE,
756
-                b'',
757
-                ssh_agent.SSHAgentFailedError,
758
-                'failed to complete the request',
759
-                id='failed-to-complete',
760
-            ),
761
-        ],
762
-    )
820
+    @Parametrizations.SIGN_ERROR_RESPONSES.value
763 821
     def test_330_sign_error_responses(
764 822
         self,
765 823
         running_ssh_agent: tests.RunningSSHAgentInfo,
... ...
@@ -782,6 +840,8 @@ class TestAgentInteraction:
782 840
         del running_ssh_agent
783 841
         passed_response_code = response_code
784 842
 
843
+        # TODO(the-13th-letter): Extract this mock function into a common
844
+        # top-level "request" mock function.
785 845
         def request(
786 846
             request_code: int | _types.SSH_AGENTC,
787 847
             payload: bytes | bytearray,
... ...
@@ -826,20 +886,7 @@ class TestAgentInteraction:
826 886
             with pytest.raises(exc_type, match=exc_pattern):
827 887
                 client.sign(key, b'abc', check_if_key_loaded=check)
828 888
 
829
-    @pytest.mark.parametrize(
830
-        ['request_code', 'response_code', 'exc_type', 'exc_pattern'],
831
-        [
832
-            pytest.param(
833
-                _types.SSH_AGENTC.REQUEST_IDENTITIES,
834
-                _types.SSH_AGENT.SUCCESS,
835
-                ssh_agent.SSHAgentFailedError,
836
-                re.escape(
837
-                    f'[Code {_types.SSH_AGENT.IDENTITIES_ANSWER.value}]'
838
-                ),
839
-                id='REQUEST_IDENTITIES-expect-SUCCESS',
840
-            ),
841
-        ],
842
-    )
889
+    @Parametrizations.REQUEST_ERROR_RESPONSES.value
843 890
     def test_340_request_error_responses(
844 891
         self,
845 892
         running_ssh_agent: tests.RunningSSHAgentInfo,
... ...
@@ -867,22 +914,7 @@ class TestAgentInteraction:
867 914
             client = stack.enter_context(ssh_agent.SSHAgentClient())
868 915
             client.request(request_code, b'', response_code=response_code)
869 916
 
870
-    @pytest.mark.parametrize(
871
-        'response_data',
872
-        [
873
-            pytest.param(b'\xde\xad\xbe\xef', id='truncated'),
874
-            pytest.param(
875
-                b'\x00\x00\x00\x0fwrong extension', id='wrong-extension'
876
-            ),
877
-            pytest.param(
878
-                b'\x00\x00\x00\x05query\xde\xad\xbe\xef', id='with-trailer'
879
-            ),
880
-            pytest.param(
881
-                b'\x00\x00\x00\x05query\x00\x00\x00\x04ext1\x00\x00',
882
-                id='with-extra-fields',
883
-            ),
884
-        ],
885
-    )
917
+    @Parametrizations.QUERY_EXTENSIONS_MALFORMED_RESPONSES.value
886 918
     def test_350_query_extensions_malformed_responses(
887 919
         self,
888 920
         monkeypatch: pytest.MonkeyPatch,
... ...
@@ -892,6 +924,9 @@ class TestAgentInteraction:
892 924
         """Fail on malformed responses while querying extensions."""
893 925
         del running_ssh_agent
894 926
 
927
+        # TODO(the-13th-letter): Extract this mock function into a common
928
+        # top-level "request" mock function after removing the
929
+        # payload-specific parts.
895 930
         def request(
896 931
             code: int | _types.SSH_AGENTC,
897 932
             payload: Buffer,
... ...
@@ -5,6 +5,7 @@
5 5
 from __future__ import annotations
6 6
 
7 7
 import copy
8
+import enum
8 9
 import math
9 10
 
10 11
 import hypothesis
... ...
@@ -66,6 +67,21 @@ def js_nested_strategy(draw: strategies.DrawFn) -> Any:
66 67
     )
67 68
 
68 69
 
70
+class Parametrizations(enum.Enum):
71
+    VALID_VAULT_TEST_CONFIGS = pytest.mark.parametrize(
72
+        'test_config',
73
+        [
74
+            conf
75
+            for conf in tests.TEST_CONFIGS
76
+            if conf.validation_settings in {None, (True,)}
77
+        ],
78
+        ids=tests._test_config_ids,
79
+    )
80
+    VAULT_TEST_CONFIGS = pytest.mark.parametrize(
81
+        'test_config', tests.TEST_CONFIGS, ids=tests._test_config_ids
82
+    )
83
+
84
+
69 85
 @hypothesis.given(value=js_nested_strategy())
70 86
 @hypothesis.example(float('nan'))
71 87
 def test_100_js_truthiness(value: Any) -> None:
... ...
@@ -85,15 +101,7 @@ def test_100_js_truthiness(value: Any) -> None:
85 101
     assert _types.js_truthiness(value) == expected
86 102
 
87 103
 
88
-@pytest.mark.parametrize(
89
-    'test_config',
90
-    [
91
-        conf
92
-        for conf in tests.TEST_CONFIGS
93
-        if conf.validation_settings in {None, (True,)}
94
-    ],
95
-    ids=tests._test_config_ids,
96
-)
104
+@Parametrizations.VALID_VAULT_TEST_CONFIGS.value
97 105
 def test_200_is_vault_config(test_config: tests.VaultTestConfig) -> None:
98 106
     """Is this vault configuration recognized as valid/invalid?
99 107
 
... ...
@@ -148,9 +156,7 @@ def test_200a_is_vault_config_smudged(
148 156
     )
149 157
 
150 158
 
151
-@pytest.mark.parametrize(
152
-    'test_config', tests.TEST_CONFIGS, ids=tests._test_config_ids
153
-)
159
+@Parametrizations.VAULT_TEST_CONFIGS.value
154 160
 def test_400_validate_vault_config(test_config: tests.VaultTestConfig) -> None:
155 161
     """Validate this vault configuration.
156 162
 
... ...
@@ -7,6 +7,7 @@
7 7
 from __future__ import annotations
8 8
 
9 9
 import array
10
+import enum
10 11
 import hashlib
11 12
 import math
12 13
 from typing import TYPE_CHECKING
... ...
@@ -27,6 +28,59 @@ if TYPE_CHECKING:
27 28
 BLOCK_SIZE = hashlib.sha1().block_size
28 29
 DIGEST_SIZE = hashlib.sha1().digest_size
29 30
 
31
+PHRASE = b'She cells C shells bye the sea shoars'
32
+"""The standard passphrase from <i>vault</i>(1)'s test suite."""
33
+GOOGLE_PHRASE = rb': 4TVH#5:aZl8LueOT\{'
34
+"""
35
+The standard derived passphrase for the "google" service, from
36
+<i>vault</i>(1)'s test suite.
37
+"""
38
+TWITTER_PHRASE = rb"[ (HN_N:lI&<ro=)3'g9"
39
+"""
40
+The standard derived passphrase for the "twitter" service, from
41
+<i>vault</i>(1)'s test suite.
42
+"""
43
+
44
+
45
+class Parametrizations(enum.Enum):
46
+    ENTROPY_RESULTS = pytest.mark.parametrize(
47
+        ['length', 'settings', 'entropy'],
48
+        [
49
+            (20, {}, math.log2(math.factorial(20)) + 20 * math.log2(94)),
50
+            (
51
+                20,
52
+                {'upper': 0, 'number': 0, 'space': 0, 'symbol': 0},
53
+                math.log2(math.factorial(20)) + 20 * math.log2(26),
54
+            ),
55
+            (0, {}, float('-inf')),
56
+            (
57
+                0,
58
+                {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0},
59
+                float('-inf'),
60
+            ),
61
+            (1, {}, math.log2(94)),
62
+            (1, {'upper': 0, 'lower': 0, 'number': 0, 'symbol': 0}, 0.0),
63
+        ],
64
+    )
65
+    BINARY_STRINGS = pytest.mark.parametrize(
66
+        's',
67
+        [
68
+            'ñ',
69
+            'Düsseldorf',
70
+            'liberté, egalité, fraternité',
71
+            'ASCII',
72
+            b'D\xc3\xbcsseldorf',
73
+            bytearray([2, 3, 5, 7, 11, 13]),
74
+        ],
75
+    )
76
+    SAMPLE_SERVICES_AND_PHRASES = pytest.mark.parametrize(
77
+        ['service', 'expected'],
78
+        [
79
+            (b'google', GOOGLE_PHRASE),
80
+            ('twitter', TWITTER_PHRASE),
81
+        ],
82
+    )
83
+
30 84
 
31 85
 def phrases_are_interchangable(
32 86
     phrase1: Buffer | str,
... ...
@@ -65,18 +119,7 @@ def phrases_are_interchangable(
65 119
 class TestVault:
66 120
     """Test passphrase derivation with the "vault" scheme."""
67 121
 
68
-    phrase = b'She cells C shells bye the sea shoars'
69
-    """The standard passphrase from <i>vault</i>(1)'s test suite."""
70
-    google_phrase = rb': 4TVH#5:aZl8LueOT\{'
71
-    """
72
-    The standard derived passphrase for the "google" service, from
73
-    <i>vault</i>(1)'s test suite.
74
-    """
75
-    twitter_phrase = rb"[ (HN_N:lI&<ro=)3'g9"
76
-    """
77
-    The standard derived passphrase for the "twitter" service, from
78
-    <i>vault</i>(1)'s test suite.
79
-    """
122
+    phrase = PHRASE
80 123
 
81 124
     @hypothesis.given(
82 125
         phrases=strategies.lists(
... ...
@@ -275,13 +318,7 @@ class TestVault:
275 318
             phrase=phrases[0], service=service
276 319
         ) == vault.Vault.create_hash(phrase=phrases[1], service=service)
277 320
 
278
-    @pytest.mark.parametrize(
279
-        ['service', 'expected'],
280
-        [
281
-            (b'google', google_phrase),
282
-            ('twitter', twitter_phrase),
283
-        ],
284
-    )
321
+    @Parametrizations.SAMPLE_SERVICES_AND_PHRASES.value
285 322
     def test_200_basic_configuration(
286 323
         self, service: bytes | str, expected: bytes
287 324
     ) -> None:
... ...
@@ -761,25 +798,7 @@ class TestVault:
761 798
         """Removing allowed characters internally works."""
762 799
         assert vault.Vault._subtract(b'be', b'abcdef') == bytearray(b'acdf')
763 800
 
764
-    @pytest.mark.parametrize(
765
-        ['length', 'settings', 'entropy'],
766
-        [
767
-            (20, {}, math.log2(math.factorial(20)) + 20 * math.log2(94)),
768
-            (
769
-                20,
770
-                {'upper': 0, 'number': 0, 'space': 0, 'symbol': 0},
771
-                math.log2(math.factorial(20)) + 20 * math.log2(26),
772
-            ),
773
-            (0, {}, float('-inf')),
774
-            (
775
-                0,
776
-                {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0},
777
-                float('-inf'),
778
-            ),
779
-            (1, {}, math.log2(94)),
780
-            (1, {'upper': 0, 'lower': 0, 'number': 0, 'symbol': 0}, 0.0),
781
-        ],
782
-    )
801
+    @Parametrizations.ENTROPY_RESULTS.value
783 802
     def test_221_entropy(
784 803
         self, length: int, settings: dict[str, int], entropy: int
785 804
     ) -> None:
... ...
@@ -809,13 +828,7 @@ class TestVault:
809 828
         assert v._entropy() == 0.0
810 829
         assert v._estimate_sufficient_hash_length() > 0
811 830
 
812
-    @pytest.mark.parametrize(
813
-        ['service', 'expected'],
814
-        [
815
-            (b'google', google_phrase),
816
-            ('twitter', twitter_phrase),
817
-        ],
818
-    )
831
+    @Parametrizations.SAMPLE_SERVICES_AND_PHRASES.value
819 832
     def test_223_hash_length_expansion(
820 833
         self,
821 834
         monkeypatch: pytest.MonkeyPatch,
... ...
@@ -834,17 +847,7 @@ class TestVault:
834 847
         assert v._estimate_sufficient_hash_length() < len(self.phrase)
835 848
         assert v.generate(service) == expected
836 849
 
837
-    @pytest.mark.parametrize(
838
-        's',
839
-        [
840
-            'ñ',
841
-            'Düsseldorf',
842
-            'liberté, egalité, fraternité',
843
-            'ASCII',
844
-            b'D\xc3\xbcsseldorf',
845
-            bytearray([2, 3, 5, 7, 11, 13]),
846
-        ],
847
-    )
850
+    @Parametrizations.BINARY_STRINGS.value
848 851
     def test_224_binary_strings(self, s: str | bytes | bytearray) -> None:
849 852
         """Byte string conversion is idempotent."""
850 853
         binstr = vault.Vault._get_binary_string
... ...
@@ -7,6 +7,7 @@
7 7
 from __future__ import annotations
8 8
 
9 9
 import contextlib
10
+import enum
10 11
 import errno
11 12
 import gettext
12 13
 import os
... ...
@@ -23,6 +24,13 @@ from derivepassphrase._internals import cli_messages as msg
23 24
 if TYPE_CHECKING:
24 25
     from collections.abc import Iterator
25 26
 
27
+
28
+class Parametrizations(enum.Enum):
29
+    MAYBE_FORMAT_STRINGS = pytest.mark.parametrize(
30
+        's', ['{spam}', '{spam}abc', '{', '}', '{{{']
31
+    )
32
+
33
+
26 34
 all_translatable_strings_dict: dict[
27 35
     msg.TranslatableString,
28 36
     msg.MsgTemplate,
... ...
@@ -203,7 +211,7 @@ class TestL10nMachineryWithDebugTranslations:
203 211
             assert ts0 != ts1
204 212
             assert len({ts0, ts1}) == 2
205 213
 
206
-    @pytest.mark.parametrize('s', ['{spam}', '{spam}abc', '{', '}', '{{{'])
214
+    @Parametrizations.MAYBE_FORMAT_STRINGS.value
207 215
     def test_102_translated_strings_suppressed_interpolation_fail(
208 216
         self,
209 217
         s: str,
210 218