using RackPeek.Domain.Discovery; namespace Tests.Discovery; /// /// Discovery runs unattended on machines nobody is watching, so its failures have to /// be legible from a log line rather than a stack trace. /// public class FailureModeTests { [Fact] public async Task An_unreachable_server_fails_with_a_network_error_not_a_crash() { // Port 1 is reserved and nothing listens on it. using var publisher = new DiscoveryPublisher("http://127.0.0.1:1", "key"); await Assert.ThrowsAnyAsync( () => publisher.PublishAsync("version: 3\nresources: []\n", false)); } [Fact] public async Task An_unreachable_docker_socket_reports_where_it_looked() { using var client = new DockerApiClient("unix:///tmp/definitely-not-a-docker.sock"); Assert.Equal("unix:///tmp/definitely-not-a-docker.sock", client.Endpoint); await Assert.ThrowsAnyAsync(() => client.ListContainersAsync()); } [Fact] public void The_default_docker_endpoint_is_the_conventional_socket() { using var client = new DockerApiClient(); // DOCKER_HOST wins when set, which is how the remote and Podman cases work. Assert.Contains("docker.sock", client.Endpoint, StringComparison.Ordinal); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void A_missing_server_or_key_resolves_to_nothing_so_validation_can_catch_it(string? value) { Assert.Null(DiscoveryPublisher.ResolveServer(value) ?? Environment.GetEnvironmentVariable(DiscoveryPublisher.ServerEnvironmentVariable)); } [Fact] public void Garbage_from_the_docker_api_does_not_take_the_process_down() => Assert.ThrowsAny(() => DockerContainerParser.Parse("not json at all")); [Fact] public void A_container_with_no_name_is_skipped_rather_than_named_badly() { List containers = DockerContainerParser.Parse(""" [ { "Id": "abc", "Image": "x", "Ports": [] }, { "Id": "def", "Names": ["/real"], "Image": "y", "Ports": [{ "PrivatePort": 80, "PublicPort": 80, "Type": "tcp" }] } ] """); Assert.Equal(["real"], containers.Select(c => c.Name)); } [Fact] public async Task A_push_against_an_unreadable_config_fails_rather_than_overwriting_it() { // The server tolerates a malformed config at boot so the web UI can be used to // fix it. A push arriving in that state must fail — merging against the empty // in-memory collection and saving would replace the user's whole inventory // with just the pushed resources. const string malformed = "version: 3\nresources:\n - kind: [not yaml"; using var api = new DiscoveryApiFixture(malformed); SystemFacts facts = SystemFactsParser.Parse(new RawSystemSnapshot { Hostname = "pusher", Cores = 2, OsName = "Debian", MemoryBytes = 4L * 1024 * 1024 * 1024, PlatformUuid = "uuid" }); await Assert.ThrowsAsync(() => api.PublishAsync(DiscoveryDocument.ToYaml([SystemResourceMapper.ToResource(facts)]))); Assert.Equal(malformed, api.StoredYaml); } [Fact] public void A_machine_with_no_network_still_produces_an_importable_resource() { SystemFacts facts = SystemFactsParser.Parse(new RawSystemSnapshot { Hostname = "offline-box", Cores = 2, OsName = "Debian", MemoryBytes = 4L * 1024 * 1024 * 1024, PlatformUuid = "uuid" }); Assert.Null(facts.Ip); // ip is optional in the schema; type, os, cores and ram are not. Fixture.AssertConformsToSchema( DiscoveryDocument.ToYaml([SystemResourceMapper.ToResource(facts)])); } }