DiscoveryIdentityTests.cs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. using RackPeek.Domain.Discovery;
  2. namespace Tests.Discovery;
  3. /// <summary>
  4. /// Identity has to be deterministic (the same machine gets the same id forever,
  5. /// with nothing stored locally) and opaque (a config file gets committed to public
  6. /// repositories, so raw machine ids and MAC addresses must not appear in it).
  7. /// </summary>
  8. public class DiscoveryIdentityTests {
  9. [Fact]
  10. public void The_same_seed_always_produces_the_same_id() =>
  11. Assert.Equal(
  12. DiscoveryId.Create(DiscoveryId.SystemScheme, "machine-a"),
  13. DiscoveryId.Create(DiscoveryId.SystemScheme, "machine-a"));
  14. [Fact]
  15. public void Different_seeds_produce_different_ids() =>
  16. Assert.NotEqual(
  17. DiscoveryId.Create(DiscoveryId.SystemScheme, "machine-a"),
  18. DiscoveryId.Create(DiscoveryId.SystemScheme, "machine-b"));
  19. [Fact]
  20. public void The_same_seed_in_different_schemes_stays_distinct() =>
  21. Assert.NotEqual(
  22. DiscoveryId.Create(DiscoveryId.SystemScheme, "seed"),
  23. DiscoveryId.Create(DiscoveryId.DockerScheme, "seed"));
  24. [Fact]
  25. public void The_seed_is_not_recoverable_by_reading_the_id() {
  26. var machineId = "7f3c9a1e5b2d4f6081a3c5e7b9d1f3a5";
  27. Assert.DoesNotContain(machineId, DiscoveryId.Create(DiscoveryId.SystemScheme, machineId));
  28. }
  29. [Fact]
  30. public void Ids_match_the_shape_the_published_schema_requires() =>
  31. Assert.Matches("^rpk[0-9]+:[a-z0-9]+:[0-9a-f]{16}$", DiscoveryId.Create("sys", "seed"));
  32. [Theory]
  33. [InlineData("")]
  34. [InlineData(" ")]
  35. public void An_empty_seed_is_refused_rather_than_producing_a_shared_id(string seed) =>
  36. Assert.Throws<ArgumentException>(() => DiscoveryId.Create("sys", seed));
  37. [Theory]
  38. [InlineData("NAS01", "nas01")]
  39. [InlineData("Tims-MacBook-Pro", "tims-macbook-pro")]
  40. [InlineData("paperless ngx", "paperless-ngx")]
  41. [InlineData("Paperless_Stack", "paperless-stack")]
  42. [InlineData(" spaced out ", "spaced-out")]
  43. [InlineData("--dashes--", "dashes")]
  44. [InlineData("!!!", "")]
  45. public void Names_are_slugged_the_way_a_person_would_type_them(string input, string expected) =>
  46. Assert.Equal(expected, DiscoveryNaming.Slug(input));
  47. [Theory]
  48. [InlineData("nas01.lan", "nas01")]
  49. [InlineData("tims-macbook-pro.local", "tims-macbook-pro")]
  50. [InlineData("nas01", "nas01")]
  51. [InlineData("", "")]
  52. public void A_host_name_is_reduced_to_its_first_label(string input, string expected) =>
  53. Assert.Equal(expected, DiscoveryNaming.HostLabel(input));
  54. [Fact]
  55. public void A_machine_with_no_usable_name_still_gets_a_deterministic_one() {
  56. var id = DiscoveryId.Create("sys", "seed");
  57. var first = DiscoveryNaming.Suggest("???", "system", id);
  58. var second = DiscoveryNaming.Suggest(null, "system", id);
  59. Assert.Equal(first, second);
  60. Assert.StartsWith("system-", first);
  61. }
  62. // RackPeek's own validation caps a resource name at 50 characters, and the import
  63. // API does not enforce it — so a longer name is accepted and then cannot be renamed
  64. // or edited from the CLI. Discovery has to stay inside the limit on its own.
  65. [Fact]
  66. public void A_long_container_name_is_cut_to_a_length_rackpeek_accepts() {
  67. var id = DiscoveryId.Create(DiscoveryId.DockerScheme, "seed");
  68. var name = DiscoveryNaming.Suggest(
  69. "homeassistant-production-stack-mosquitto-broker-primary-1", "service", id);
  70. Assert.True(name.Length <= DiscoveryNaming.MaxNameLength, name);
  71. Assert.DoesNotContain("--", name);
  72. Assert.False(name.EndsWith('-'));
  73. }
  74. [Fact]
  75. public void A_disambiguating_suffix_shortens_the_name_to_make_room() {
  76. var suffixed = DiscoveryNaming.WithSuffix(new string('a', 48), "a3f9c2e1");
  77. Assert.True(suffixed.Length <= DiscoveryNaming.MaxNameLength, suffixed);
  78. Assert.EndsWith("-a3f9c2e1", suffixed);
  79. }
  80. [Fact]
  81. public void Two_names_that_truncate_alike_stay_distinct() {
  82. // Compose puts the replica index last, which is exactly what truncation removes.
  83. var taken = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  84. var prefix = "homeassistant-production-stack-mosquitto-broker-primary";
  85. var first = DiscoveryNaming.Unique(
  86. DiscoveryNaming.Suggest($"{prefix}-1", "service", DiscoveryId.Create("docker", "a")),
  87. DiscoveryId.Create("docker", "a"),
  88. taken);
  89. var second = DiscoveryNaming.Unique(
  90. DiscoveryNaming.Suggest($"{prefix}-2", "service", DiscoveryId.Create("docker", "b")),
  91. DiscoveryId.Create("docker", "b"),
  92. taken);
  93. Assert.NotEqual(first, second);
  94. Assert.True(second.Length <= DiscoveryNaming.MaxNameLength, second);
  95. }
  96. [Theory]
  97. [InlineData("nas01")]
  98. [InlineData("a-really-long-hostname-that-somebody-actually-configured-somewhere")]
  99. [InlineData("!!!")]
  100. [InlineData("")]
  101. public void Every_suggested_name_is_valid_to_rackpeek(string input) {
  102. var id = DiscoveryId.Create(DiscoveryId.SystemScheme, "seed");
  103. var name = DiscoveryNaming.Suggest(input, "system", id);
  104. // The same checks ThrowIfInvalid.ResourceName makes.
  105. Assert.False(string.IsNullOrWhiteSpace(name));
  106. Assert.True(name.Length <= DiscoveryNaming.MaxNameLength, name);
  107. }
  108. [Theory]
  109. [InlineData("日本語サーバー")]
  110. [InlineData("сервер")]
  111. [InlineData("🎉🎉🎉")]
  112. public void A_name_with_nothing_ascii_in_it_falls_back_to_the_id(string input) {
  113. var id = DiscoveryId.Create(DiscoveryId.SystemScheme, "seed");
  114. var name = DiscoveryNaming.Suggest(input, "system", id);
  115. // Slugging keeps to ASCII, so these produce nothing usable and the id-derived
  116. // name takes over — still deterministic, still valid, still unique.
  117. Assert.Equal($"system-{DiscoveryId.ShortSuffix(id)}", name);
  118. }
  119. }