using RackPeek.Domain.Discovery; namespace Tests.Discovery; /// /// Identity has to be deterministic (the same machine gets the same id forever, /// with nothing stored locally) and opaque (a config file gets committed to public /// repositories, so raw machine ids and MAC addresses must not appear in it). /// public class DiscoveryIdentityTests { [Fact] public void The_same_seed_always_produces_the_same_id() => Assert.Equal( DiscoveryId.Create(DiscoveryId.SystemScheme, "machine-a"), DiscoveryId.Create(DiscoveryId.SystemScheme, "machine-a")); [Fact] public void Different_seeds_produce_different_ids() => Assert.NotEqual( DiscoveryId.Create(DiscoveryId.SystemScheme, "machine-a"), DiscoveryId.Create(DiscoveryId.SystemScheme, "machine-b")); [Fact] public void The_same_seed_in_different_schemes_stays_distinct() => Assert.NotEqual( DiscoveryId.Create(DiscoveryId.SystemScheme, "seed"), DiscoveryId.Create(DiscoveryId.DockerScheme, "seed")); [Fact] public void The_seed_is_not_recoverable_by_reading_the_id() { var machineId = "7f3c9a1e5b2d4f6081a3c5e7b9d1f3a5"; Assert.DoesNotContain(machineId, DiscoveryId.Create(DiscoveryId.SystemScheme, machineId)); } [Fact] public void Ids_match_the_shape_the_published_schema_requires() => Assert.Matches("^rpk[0-9]+:[a-z0-9]+:[0-9a-f]{16}$", DiscoveryId.Create("sys", "seed")); [Theory] [InlineData("")] [InlineData(" ")] public void An_empty_seed_is_refused_rather_than_producing_a_shared_id(string seed) => Assert.Throws(() => DiscoveryId.Create("sys", seed)); [Theory] [InlineData("NAS01", "nas01")] [InlineData("Tims-MacBook-Pro", "tims-macbook-pro")] [InlineData("paperless ngx", "paperless-ngx")] [InlineData("Paperless_Stack", "paperless-stack")] [InlineData(" spaced out ", "spaced-out")] [InlineData("--dashes--", "dashes")] [InlineData("!!!", "")] public void Names_are_slugged_the_way_a_person_would_type_them(string input, string expected) => Assert.Equal(expected, DiscoveryNaming.Slug(input)); [Theory] [InlineData("nas01.lan", "nas01")] [InlineData("tims-macbook-pro.local", "tims-macbook-pro")] [InlineData("nas01", "nas01")] [InlineData("", "")] public void A_host_name_is_reduced_to_its_first_label(string input, string expected) => Assert.Equal(expected, DiscoveryNaming.HostLabel(input)); [Fact] public void A_machine_with_no_usable_name_still_gets_a_deterministic_one() { var id = DiscoveryId.Create("sys", "seed"); var first = DiscoveryNaming.Suggest("???", "system", id); var second = DiscoveryNaming.Suggest(null, "system", id); Assert.Equal(first, second); Assert.StartsWith("system-", first); } // RackPeek's own validation caps a resource name at 50 characters, and the import // API does not enforce it — so a longer name is accepted and then cannot be renamed // or edited from the CLI. Discovery has to stay inside the limit on its own. [Fact] public void A_long_container_name_is_cut_to_a_length_rackpeek_accepts() { var id = DiscoveryId.Create(DiscoveryId.DockerScheme, "seed"); var name = DiscoveryNaming.Suggest( "homeassistant-production-stack-mosquitto-broker-primary-1", "service", id); Assert.True(name.Length <= DiscoveryNaming.MaxNameLength, name); Assert.DoesNotContain("--", name); Assert.False(name.EndsWith('-')); } [Fact] public void A_disambiguating_suffix_shortens_the_name_to_make_room() { var suffixed = DiscoveryNaming.WithSuffix(new string('a', 48), "a3f9c2e1"); Assert.True(suffixed.Length <= DiscoveryNaming.MaxNameLength, suffixed); Assert.EndsWith("-a3f9c2e1", suffixed); } [Fact] public void Two_names_that_truncate_alike_stay_distinct() { // Compose puts the replica index last, which is exactly what truncation removes. var taken = new HashSet(StringComparer.OrdinalIgnoreCase); var prefix = "homeassistant-production-stack-mosquitto-broker-primary"; var first = DiscoveryNaming.Unique( DiscoveryNaming.Suggest($"{prefix}-1", "service", DiscoveryId.Create("docker", "a")), DiscoveryId.Create("docker", "a"), taken); var second = DiscoveryNaming.Unique( DiscoveryNaming.Suggest($"{prefix}-2", "service", DiscoveryId.Create("docker", "b")), DiscoveryId.Create("docker", "b"), taken); Assert.NotEqual(first, second); Assert.True(second.Length <= DiscoveryNaming.MaxNameLength, second); } [Theory] [InlineData("nas01")] [InlineData("a-really-long-hostname-that-somebody-actually-configured-somewhere")] [InlineData("!!!")] [InlineData("")] public void Every_suggested_name_is_valid_to_rackpeek(string input) { var id = DiscoveryId.Create(DiscoveryId.SystemScheme, "seed"); var name = DiscoveryNaming.Suggest(input, "system", id); // The same checks ThrowIfInvalid.ResourceName makes. Assert.False(string.IsNullOrWhiteSpace(name)); Assert.True(name.Length <= DiscoveryNaming.MaxNameLength, name); } [Theory] [InlineData("日本語サーバー")] [InlineData("сервер")] [InlineData("🎉🎉🎉")] public void A_name_with_nothing_ascii_in_it_falls_back_to_the_id(string input) { var id = DiscoveryId.Create(DiscoveryId.SystemScheme, "seed"); var name = DiscoveryNaming.Suggest(input, "system", id); // Slugging keeps to ASCII, so these produce nothing usable and the id-derived // name takes over — still deterministic, still valid, still unique. Assert.Equal($"system-{DiscoveryId.ShortSuffix(id)}", name); } }