CorruptConfigTests.cs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. using Tests.EndToEnd.Infra;
  2. using Xunit.Abstractions;
  3. namespace Tests.EndToEnd;
  4. // Reproduces the load-side half of
  5. // https://github.com/Timmoth/RackPeek/issues/337: saves rewrite config.yaml in
  6. // place (File.WriteAllTextAsync truncates before writing, and never flushes),
  7. // so an interrupted save leaves a truncated file behind — and a truncated file
  8. // is then accepted without complaint on the next load.
  9. //
  10. // The writer-side half (make PhysicalTextFileStore write atomically and
  11. // durably: temp file + flush + rename) is not observable from a black-box
  12. // test; these tests pin the user-facing contract that a damaged file must not
  13. // be served silently or crash with a raw stack trace.
  14. [Collection("Yaml CLI tests")]
  15. public class CorruptConfigTests(TempYamlCliFixture fs, ITestOutputHelper outputHelper)
  16. : IClassFixture<TempYamlCliFixture> {
  17. // Exactly what the serializer writes for three bare servers.
  18. private const string _fullConfig = """
  19. version: 4
  20. resources:
  21. - kind: Server
  22. name: srv-a
  23. - kind: Server
  24. name: srv-b
  25. - kind: Server
  26. name: srv-c
  27. connections: []
  28. """;
  29. private async Task<string> ExecuteAsync(params string[] args) {
  30. outputHelper.WriteLine($"rpk {string.Join(" ", args)}");
  31. var output = await YamlCliTestHost.RunAsync(
  32. args,
  33. fs.Root,
  34. outputHelper,
  35. "config.yaml");
  36. outputHelper.WriteLine(output);
  37. return output;
  38. }
  39. [Fact]
  40. public async Task a_config_truncated_at_a_resource_boundary_is_not_served_silently() {
  41. // Simulate an interrupted in-place save: the file ends mid-way through
  42. // the resources list. This still parses — as a plausible, smaller
  43. // inventory missing srv-c and the connections section.
  44. var truncated = _fullConfig[.._fullConfig.IndexOf("- kind: Server\n name: srv-c", StringComparison.Ordinal)];
  45. await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), truncated);
  46. var output = await ExecuteAsync("summary");
  47. // Serving a structurally incomplete file (a resources document with no
  48. // connections section — something RackPeek's own serializer never
  49. // writes) with no diagnostic at all is how a truncation becomes silent
  50. // data loss: the next save persists the smaller inventory as if it
  51. // were intentional.
  52. var hasDiagnostic =
  53. output.Contains("error", StringComparison.OrdinalIgnoreCase)
  54. || output.Contains("warn", StringComparison.OrdinalIgnoreCase)
  55. || output.Contains("corrupt", StringComparison.OrdinalIgnoreCase)
  56. || output.Contains("incomplete", StringComparison.OrdinalIgnoreCase)
  57. || output.Contains("truncated", StringComparison.OrdinalIgnoreCase);
  58. Assert.True(hasDiagnostic,
  59. $"A truncated config was served with no diagnostic. Output:\n{output}");
  60. }
  61. [Fact]
  62. public async Task a_config_cut_mid_token_fails_with_a_friendly_error() {
  63. // Simulate a save that died mid-write inside a YAML token.
  64. var truncated = _fullConfig[.._fullConfig.IndexOf("nd: Server\n name: srv-c", StringComparison.Ordinal)];
  65. await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), truncated);
  66. var output = await ExecuteAsync("summary");
  67. // Observed on staging: the unparseable file is swallowed entirely and
  68. // `rpk summary` reports an EMPTY inventory (Hardware (0)) with exit 0 —
  69. // no error, no mention of the file. That is the worst outcome for
  70. // #337: a later write would persist the empty inventory over the
  71. // damaged-but-recoverable file. The user should instead get an
  72. // actionable message naming the config file, and no stack dump.
  73. Assert.DoesNotContain("at RackPeek.", output);
  74. Assert.DoesNotContain("YamlDotNet.Core", output);
  75. Assert.Contains("config.yaml", output);
  76. }
  77. }