Add xfailing tests for indentation and line breaks in vault configurations
Marco Ricci

Marco Ricci commited on 2025-02-05 11:44:22
Zeige 1 geänderte Dateien mit 124 Einfügungen und 18 Löschungen.


Though the on-disk vault configurations are not intended to be managed
by the user, it is valuable for debugging and lay examination for them
to be readable and editable anyway; doubly so if the configuration is
being exported.  So ensure that when writing out the configuration, we
add indents and linebreaks.

This commit only adds the xfailing tests for this functionality, not the
implementation itself.  Additionally, since there was no simple test
akin to config importing but for config exporting, we added the latter
on the basis of the former, and renumbered the other export (failure)
tests accordingly.
... ...
@@ -236,6 +236,46 @@ def is_harmless_config_import_warning(record: tuple[str, int, str]) -> bool:
236 236
     return any(tests.warning_emitted(w, [record]) for w in possible_warnings)
237 237
 
238 238
 
239
+def assert_vault_config_is_indented_and_line_broken(
240
+    config_txt: str,
241
+    /,
242
+) -> None:
243
+    """Return true if the vault configuration is indented and line broken.
244
+
245
+    Indented and rewrapped vault configurations as produced by
246
+    `json.dump` contain the closing '}' of the '$.services' object
247
+    on a separate, indented line:
248
+
249
+    ~~~~
250
+    {
251
+      "services": {
252
+        ...
253
+      }  <-- this brace here
254
+    }
255
+    ~~~~
256
+
257
+    or, if there are no services, then the indented line
258
+
259
+    ~~~~
260
+      "services": {}
261
+    ~~~~
262
+
263
+    Both variations may end with a comma if there are more top-level
264
+    keys.
265
+
266
+    """
267
+    known_indented_lines = {
268
+        '}',
269
+        '},',
270
+        '"services": {}',
271
+        '"services": {},',
272
+    }
273
+    assert any([
274
+        line.strip() in known_indented_lines and line.startswith((' ', '\t'))
275
+        for line in config_txt.splitlines()
276
+    ])
277
+
278
+
239 279
 def vault_config_exporter_shell_interpreter(  # noqa: C901
240 280
     script: str | Iterable[str],
241 281
     /,
... ...
@@ -2071,6 +2111,10 @@ class TestCLI:
2071 2111
             'expected error exit and known error message'
2072 2112
         )
2073 2113
 
2114
+    @pytest.mark.xfail(
2115
+        reason='config are currently stored as single lines',
2116
+        raises=AssertionError,
2117
+    )
2074 2118
     @Parametrize.VALID_TEST_CONFIGS
2075 2119
     def test_213_import_config_success(
2076 2120
         self,
... ...
@@ -2097,17 +2141,22 @@ class TestCLI:
2097 2141
                 input=json.dumps(config),
2098 2142
                 catch_exceptions=False,
2099 2143
             )
2100
-            with cli_helpers.config_filename(subsystem='vault').open(
2101
-                encoding='UTF-8'
2102
-            ) as infile:
2103
-                config2 = json.load(infile)
2144
+            config_txt = cli_helpers.config_filename(
2145
+                subsystem='vault'
2146
+            ).read_text(encoding='UTF-8')
2147
+            config2 = json.loads(config_txt)
2104 2148
         result = tests.ReadableResult.parse(result_)
2105 2149
         assert result.clean_exit(empty_stderr=False), 'expected clean exit'
2106 2150
         assert config2 == config, 'config not imported correctly'
2107 2151
         assert not result.stderr or all(  # pragma: no branch
2108 2152
             map(is_harmless_config_import_warning, caplog.record_tuples)
2109 2153
         ), 'unexpected error output'
2154
+        assert_vault_config_is_indented_and_line_broken(config_txt)
2110 2155
 
2156
+    @pytest.mark.xfail(
2157
+        reason='config are currently stored as single lines',
2158
+        raises=AssertionError,
2159
+    )
2111 2160
     @hypothesis.settings(
2112 2161
         suppress_health_check=[
2113 2162
             *hypothesis.settings().suppress_health_check,
... ...
@@ -2155,16 +2204,17 @@ class TestCLI:
2155 2204
                 input=json.dumps(config),
2156 2205
                 catch_exceptions=False,
2157 2206
             )
2158
-            with cli_helpers.config_filename(subsystem='vault').open(
2159
-                encoding='UTF-8'
2160
-            ) as infile:
2161
-                config3 = json.load(infile)
2207
+            config_txt = cli_helpers.config_filename(
2208
+                subsystem='vault'
2209
+            ).read_text(encoding='UTF-8')
2210
+            config3 = json.loads(config_txt)
2162 2211
         result = tests.ReadableResult.parse(result_)
2163 2212
         assert result.clean_exit(empty_stderr=False), 'expected clean exit'
2164 2213
         assert config3 == config2, 'config not imported correctly'
2165 2214
         assert not result.stderr or all(
2166 2215
             map(is_harmless_config_import_warning, caplog.record_tuples)
2167 2216
         ), 'unexpected error output'
2217
+        assert_vault_config_is_indented_and_line_broken(config_txt)
2168 2218
 
2169 2219
     def test_213b_import_bad_config_not_vault_config(
2170 2220
         self,
... ...
@@ -2256,8 +2306,54 @@ class TestCLI:
2256 2306
             'expected error exit and known error message'
2257 2307
         )
2258 2308
 
2309
+    @pytest.mark.xfail(
2310
+        reason='config exports are currently single-line',
2311
+        raises=AssertionError,
2312
+    )
2313
+    @Parametrize.VALID_TEST_CONFIGS
2314
+    def test_214_export_config_success(
2315
+        self,
2316
+        caplog: pytest.LogCaptureFixture,
2317
+        config: Any,
2318
+    ) -> None:
2319
+        """Exporting a configuration works."""
2320
+        runner = click.testing.CliRunner(mix_stderr=False)
2321
+        # TODO(the-13th-letter): Rewrite using parenthesized
2322
+        # with-statements.
2323
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2324
+        with contextlib.ExitStack() as stack:
2325
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2326
+            stack.enter_context(
2327
+                tests.isolated_vault_config(
2328
+                    monkeypatch=monkeypatch,
2329
+                    runner=runner,
2330
+                    vault_config=config,
2331
+                )
2332
+            )
2333
+            with cli_helpers.config_filename(subsystem='vault').open(
2334
+                'w', encoding='UTF-8'
2335
+            ) as outfile:
2336
+                # Ensure the config is written on one line.
2337
+                json.dump(config, outfile, indent=None)
2338
+            result_ = runner.invoke(
2339
+                cli.derivepassphrase_vault,
2340
+                ['--export', '-'],
2341
+                catch_exceptions=False,
2342
+            )
2343
+            with cli_helpers.config_filename(subsystem='vault').open(
2344
+                encoding='UTF-8'
2345
+            ) as infile:
2346
+                config2 = json.load(infile)
2347
+        result = tests.ReadableResult.parse(result_)
2348
+        assert result.clean_exit(empty_stderr=False), 'expected clean exit'
2349
+        assert config2 == config, 'config not imported correctly'
2350
+        assert not result.stderr or all(  # pragma: no branch
2351
+            map(is_harmless_config_import_warning, caplog.record_tuples)
2352
+        ), 'unexpected error output'
2353
+        assert_vault_config_is_indented_and_line_broken(result.output)
2354
+
2259 2355
     @Parametrize.EXPORT_FORMAT_OPTIONS
2260
-    def test_214_export_settings_no_stored_settings(
2356
+    def test_214a_export_settings_no_stored_settings(
2261 2357
         self,
2262 2358
         export_options: list[str],
2263 2359
     ) -> None:
... ...
@@ -2290,7 +2386,7 @@ class TestCLI:
2290 2386
         assert result.clean_exit(empty_stderr=True), 'expected clean exit'
2291 2387
 
2292 2388
     @Parametrize.EXPORT_FORMAT_OPTIONS
2293
-    def test_214a_export_settings_bad_stored_config(
2389
+    def test_214b_export_settings_bad_stored_config(
2294 2390
         self,
2295 2391
         export_options: list[str],
2296 2392
     ) -> None:
... ...
@@ -2320,7 +2416,7 @@ class TestCLI:
2320 2416
         )
2321 2417
 
2322 2418
     @Parametrize.EXPORT_FORMAT_OPTIONS
2323
-    def test_214b_export_settings_not_a_file(
2419
+    def test_214c_export_settings_not_a_file(
2324 2420
         self,
2325 2421
         export_options: list[str],
2326 2422
     ) -> None:
... ...
@@ -2352,7 +2448,7 @@ class TestCLI:
2352 2448
         )
2353 2449
 
2354 2450
     @Parametrize.EXPORT_FORMAT_OPTIONS
2355
-    def test_214c_export_settings_target_not_a_file(
2451
+    def test_214d_export_settings_target_not_a_file(
2356 2452
         self,
2357 2453
         export_options: list[str],
2358 2454
     ) -> None:
... ...
@@ -2382,7 +2478,7 @@ class TestCLI:
2382 2478
         )
2383 2479
 
2384 2480
     @Parametrize.EXPORT_FORMAT_OPTIONS
2385
-    def test_214d_export_settings_settings_directory_not_a_directory(
2481
+    def test_214e_export_settings_settings_directory_not_a_directory(
2386 2482
         self,
2387 2483
         export_options: list[str],
2388 2484
     ) -> None:
... ...
@@ -2559,6 +2655,10 @@ contents go here
2559 2655
                 config = json.load(infile)
2560 2656
             assert config == {'global': {'phrase': 'abc'}, 'services': {}}
2561 2657
 
2658
+    @pytest.mark.xfail(
2659
+        reason='config are currently stored as single lines',
2660
+        raises=AssertionError,
2661
+    )
2562 2662
     @Parametrize.CONFIG_EDITING_VIA_CONFIG_FLAG
2563 2663
     def test_224_store_config_good(
2564 2664
         self,
... ...
@@ -2566,7 +2666,12 @@ contents go here
2566 2666
         input: str,
2567 2667
         result_config: Any,
2568 2668
     ) -> None:
2569
-        """Storing valid settings via `--config` works."""
2669
+        """Storing valid settings via `--config` works.
2670
+
2671
+        The format also contains embedded newlines and indentation to make
2672
+        the config more readable.
2673
+
2674
+        """
2570 2675
         runner = click.testing.CliRunner(mix_stderr=False)
2571 2676
         # TODO(the-13th-letter): Rewrite using parenthesized
2572 2677
         # with-statements.
... ...
@@ -2591,13 +2696,14 @@ contents go here
2591 2696
             )
2592 2697
             result = tests.ReadableResult.parse(result_)
2593 2698
             assert result.clean_exit(), 'expected clean exit'
2594
-            with cli_helpers.config_filename(subsystem='vault').open(
2595
-                encoding='UTF-8'
2596
-            ) as infile:
2597
-                config = json.load(infile)
2699
+            config_txt = cli_helpers.config_filename(
2700
+                subsystem='vault'
2701
+            ).read_text(encoding='UTF-8')
2702
+            config = json.loads(config_txt)
2598 2703
             assert config == result_config, (
2599 2704
                 'stored config does not match expectation'
2600 2705
             )
2706
+            assert_vault_config_is_indented_and_line_broken(config_txt)
2601 2707
 
2602 2708
     @Parametrize.CONFIG_EDITING_VIA_CONFIG_FLAG_FAILURES
2603 2709
     def test_225_store_config_fail(
2604 2710