using PnvPanel.Domain.Exceptions; using PnvPanel.Domain.Nodes; using Xunit; namespace PnvPanel.Domain.Tests.Nodes; public class NodeTests { private static NodeCredentials Credentials => new("admin", "protected-secret"); [Fact] public void Register_WithAbsoluteUri_CreatesEnabledUnknownStatusNode() { var node = Node.Register( "Germany-1", new Uri("https://de1.example.com:2053"), Credentials, "Germany" ); Assert.Equal("Germany-1", node.Name); Assert.Equal("Germany", node.Location); Assert.Equal(NodeStatus.Unknown, node.Status); Assert.True(node.IsEnabled); Assert.NotEqual(Guid.Empty, node.Id); } [Fact] public void Register_WithRelativeUri_Throws() { var relativeUri = new Uri("de1.example.com", UriKind.Relative); Assert.Throws(() => Node.Register("Germany-1", relativeUri, Credentials, null) ); } [Theory] [InlineData("https://host.example.com/benis", "https://host.example.com/benis/")] [InlineData("https://host.example.com/benis/", "https://host.example.com/benis/")] [InlineData("https://host.example.com", "https://host.example.com/")] public void Register_NormalizesBaseAddressToTrailingSlash(string input, string expected) { var node = Node.Register("Node", new Uri(input), Credentials, null); Assert.Equal(expected, node.BaseAddress.ToString()); } [Theory] [InlineData("https://host.example.com/benis", "https://host.example.com/benis/")] [InlineData("https://host.example.com/benis/", "https://host.example.com/benis/")] public void UpdateAddress_NormalizesBaseAddressToTrailingSlash(string input, string expected) { var node = Node.Register("Node", new Uri("https://old.example.com"), Credentials, null); node.UpdateAddress(new Uri(input)); Assert.Equal(expected, node.BaseAddress.ToString()); } [Fact] public void UpdateDetails_ChangesNameAndLocation() { var node = Node.Register( "Old", new Uri("https://example.com"), Credentials, "Old location" ); node.UpdateDetails("New", "New location"); Assert.Equal("New", node.Name); Assert.Equal("New location", node.Location); } [Fact] public void UpdateCredentials_ReplacesCredentials() { var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null); var newCredentials = new NodeCredentials("root", "new-protected-secret"); node.UpdateCredentials(newCredentials); Assert.Equal(newCredentials, node.Credentials); } [Fact] public void Disable_ThenEnable_TogglesIsEnabled() { var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null); node.Disable(); Assert.False(node.IsEnabled); node.Enable(); Assert.True(node.IsEnabled); } [Fact] public void SetNotifyOnStatusChange_DefaultsFalse_AndCanBeToggled() { var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null); Assert.False(node.NotifyOnStatusChange); node.SetNotifyOnStatusChange(true); Assert.True(node.NotifyOnStatusChange); node.SetNotifyOnStatusChange(false); Assert.False(node.NotifyOnStatusChange); } [Fact] public void UpdateStatus_SetsStatus() { var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null); node.UpdateStatus(NodeStatus.Online); Assert.Equal(NodeStatus.Online, node.Status); } [Fact] public void RecordProbe_SingleFailure_DoesNotGoOffline_WhenThresholdNotReached() { var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null); node.UpdateStatus(NodeStatus.Online); var changed = node.RecordProbe(reachable: false, failureThreshold: 2); Assert.False(changed); Assert.Equal(NodeStatus.Online, node.Status); Assert.Equal(1, node.ConsecutiveProbeFailures); } [Fact] public void RecordProbe_GoesOffline_OnlyAfterConsecutiveFailuresReachThreshold() { var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null); node.UpdateStatus(NodeStatus.Online); Assert.False(node.RecordProbe(reachable: false, failureThreshold: 2)); Assert.Equal(NodeStatus.Online, node.Status); var changed = node.RecordProbe(reachable: false, failureThreshold: 2); Assert.True(changed); Assert.Equal(NodeStatus.Offline, node.Status); Assert.Equal(2, node.ConsecutiveProbeFailures); } [Fact] public void RecordProbe_FlappingFailSuccess_NeverGoesOffline() { var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null); node.UpdateStatus(NodeStatus.Online); // Чередование fail/success (ровно паттерн со скриншота) не должно ронять статус: // порогу нужны ДВЕ подряд неудачи, а удачная проба обнуляет счётчик. for (var i = 0; i < 5; i++) { Assert.False(node.RecordProbe(reachable: false, failureThreshold: 2)); Assert.False(node.RecordProbe(reachable: true, failureThreshold: 2)); Assert.Equal(NodeStatus.Online, node.Status); Assert.Equal(0, node.ConsecutiveProbeFailures); } } [Fact] public void RecordProbe_SuccessAfterOffline_ReturnsOnlineImmediately_AndResetsCounter() { var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null); node.RecordProbe(reachable: false, failureThreshold: 2); node.RecordProbe(reachable: false, failureThreshold: 2); Assert.Equal(NodeStatus.Offline, node.Status); var changed = node.RecordProbe(reachable: true, failureThreshold: 2); Assert.True(changed); Assert.Equal(NodeStatus.Online, node.Status); Assert.Equal(0, node.ConsecutiveProbeFailures); } [Fact] public void RecordProbe_SuccessWhenAlreadyOnline_ReportsNoChange() { var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null); node.UpdateStatus(NodeStatus.Online); var changed = node.RecordProbe(reachable: true, failureThreshold: 2); Assert.False(changed); Assert.Equal(NodeStatus.Online, node.Status); } [Fact] public void MarkSynced_SetsLastSyncAt() { var node = Node.Register("Node", new Uri("https://example.com"), Credentials, null); Assert.Null(node.LastSyncAt); node.MarkSynced(); Assert.NotNull(node.LastSyncAt); } [Fact] public void NodeCredentials_ToString_RedactsPassword() { var text = Credentials.ToString(); Assert.DoesNotContain("protected-secret", text); Assert.Contains("REDACTED", text); } }