diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 6ed9b0c..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,21 +0,0 @@ -pipeline { - agent { - label '.net7.0' - } - stages { - stage('Build classlib') { - steps { - sh '''cd classlib - dotnet build --no-incremental --force --configuration Release - dotnet publish -c Release''' - - contentReplace(configs: [fileContentReplaceConfig(configs: [fileContentReplaceItemConfig(replace: 'ModuleVersion = \'2.0.$BUILD_NUMBER\'', search: 'ModuleVersion = \'2.0\'')], fileEncoding: 'UTF-8', filePath: 'ps.ipam.psd1')]) - } - } - } - post { - success { - archiveArtifacts artifacts: 'classlib/bin/Release/netstandard2.1/publish/*.dll, *.psd1, *.psm1, LICENSE, **/*.ps1xml, **/*.ps1', followSymlinks: false, onlyIfSuccessful: true - } - } -} \ No newline at end of file diff --git a/classlib.tests/Mocks/CmdletTestHelper.cs b/classlib.tests/Mocks/CmdletTestHelper.cs new file mode 100644 index 0000000..f1c061b --- /dev/null +++ b/classlib.tests/Mocks/CmdletTestHelper.cs @@ -0,0 +1,139 @@ +namespace PS.IPAM.Tests.Mocks; + +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Reflection; + +/// +/// Helper class for testing PowerShell cmdlets. +/// +public class CmdletTestHelper : IDisposable where T : PSCmdlet, new() +{ + private readonly T _cmdlet; + private readonly List _output = new(); + private readonly List _errors = new(); + private bool _disposed; + + public CmdletTestHelper() + { + _cmdlet = new T(); + } + + /// + /// Gets the cmdlet instance for setting parameters. + /// + public T Cmdlet => _cmdlet; + + /// + /// Gets the output objects written by the cmdlet. + /// + public IReadOnlyList Output => _output.AsReadOnly(); + + /// + /// Gets the errors written by the cmdlet. + /// + public IReadOnlyList Errors => _errors.AsReadOnly(); + + /// + /// Gets whether any errors were written. + /// + public bool HasErrors => _errors.Count > 0; + + /// + /// Invokes the cmdlet and captures output. + /// + public void Invoke() + { + // Use reflection to call the protected ProcessRecord method + var processMethod = typeof(T).GetMethod("ProcessRecord", + BindingFlags.NonPublic | BindingFlags.Instance); + + if (processMethod == null) + { + throw new InvalidOperationException("ProcessRecord method not found on cmdlet."); + } + + // Set up a mock command runtime to capture output + var runtime = new MockCommandRuntime(_output, _errors); + var runtimeProperty = typeof(Cmdlet).GetProperty("CommandRuntime", + BindingFlags.Public | BindingFlags.Instance); + + runtimeProperty?.SetValue(_cmdlet, runtime); + + // Invoke the method + try + { + processMethod.Invoke(_cmdlet, null); + } + catch (TargetInvocationException ex) when (ex.InnerException != null) + { + // Unwrap the exception + throw ex.InnerException; + } + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + } + } +} + +/// +/// Mock implementation of ICommandRuntime for capturing cmdlet output. +/// This is a minimal implementation that only handles the methods we need for testing. +/// +internal class MockCommandRuntime : ICommandRuntime +{ + private readonly List _output; + private readonly List _errors; + + public MockCommandRuntime(List output, List errors) + { + _output = output; + _errors = errors; + } + + public PSHost Host => null!; + public PSTransactionContext CurrentPSTransaction => null!; + + public bool ShouldContinue(string query, string caption) => true; + public bool ShouldContinue(string query, string caption, ref bool yesToAll, ref bool noToAll) => true; + public bool ShouldProcess(string target) => true; + public bool ShouldProcess(string target, string action) => true; + public bool ShouldProcess(string verboseDescription, string verboseWarning, string caption) => true; + public bool ShouldProcess(string verboseDescription, string verboseWarning, string caption, out ShouldProcessReason shouldProcessReason) + { + shouldProcessReason = ShouldProcessReason.None; + return true; + } + + public bool TransactionAvailable() => false; + + public void ThrowTerminatingError(ErrorRecord errorRecord) => throw errorRecord.Exception; + + public void WriteCommandDetail(string text) { } + public void WriteDebug(string text) { } + public void WriteError(ErrorRecord errorRecord) => _errors.Add(errorRecord); + public void WriteObject(object sendToPipeline) => _output.Add(sendToPipeline); + public void WriteObject(object sendToPipeline, bool enumerateCollection) + { + if (enumerateCollection && sendToPipeline is System.Collections.IEnumerable enumerable) + { + foreach (var item in enumerable) + { + _output.Add(item); + } + } + else + { + _output.Add(sendToPipeline); + } + } + public void WriteProgress(ProgressRecord progressRecord) { } + public void WriteProgress(long sourceId, ProgressRecord progressRecord) { } + public void WriteVerbose(string text) { } + public void WriteWarning(string text) { } +} diff --git a/classlib.tests/Mocks/MockHttpMessageHandler.cs b/classlib.tests/Mocks/MockHttpMessageHandler.cs new file mode 100644 index 0000000..497a788 --- /dev/null +++ b/classlib.tests/Mocks/MockHttpMessageHandler.cs @@ -0,0 +1,172 @@ +namespace PS.IPAM.Tests.Mocks; + +using System.Net; + +/// +/// A mock HTTP message handler for testing HTTP requests without making actual network calls. +/// This handler does not dispose itself when the HttpClient is disposed, allowing reuse in tests. +/// +public class MockHttpMessageHandler : HttpMessageHandler +{ + private readonly Queue _responses = new(); + private readonly List _requests = new(); + private bool _disposed = false; + + /// + /// Gets all requests that were sent through this handler. + /// + public IReadOnlyList Requests => _requests.AsReadOnly(); + + /// + /// Gets the last request that was sent through this handler. + /// + public HttpRequestMessage? LastRequest => _requests.LastOrDefault(); + + /// + /// Queues a response to be returned for the next request. + /// + public MockHttpMessageHandler WithResponse(HttpStatusCode statusCode, string content, string contentType = "application/json") + { + _responses.Enqueue(new MockResponse(statusCode, content, contentType)); + return this; + } + + /// + /// Queues a successful JSON response. + /// + public MockHttpMessageHandler WithJsonResponse(string jsonContent) + { + return WithResponse(HttpStatusCode.OK, jsonContent, "application/json"); + } + + /// + /// Queues a successful response with phpIPAM-style wrapper. + /// + public MockHttpMessageHandler WithSuccessResponse(string dataJson) + { + var response = $"{{\"code\":200,\"success\":true,\"data\":{dataJson}}}"; + return WithJsonResponse(response); + } + + /// + /// Queues a 404 Not Found response. + /// + public MockHttpMessageHandler WithNotFoundResponse() + { + return WithResponse(HttpStatusCode.NotFound, "{\"code\":404,\"success\":false,\"message\":\"Not found\"}", "application/json"); + } + + /// + /// Queues an error response. + /// + public MockHttpMessageHandler WithErrorResponse(HttpStatusCode statusCode, string message) + { + var response = $"{{\"code\":{(int)statusCode},\"success\":false,\"message\":\"{message}\"}}"; + return WithResponse(statusCode, response, "application/json"); + } + + /// + /// Queues an exception to be thrown on the next request. + /// + public MockHttpMessageHandler WithException(Exception exception) + { + _responses.Enqueue(new MockResponse(exception)); + return this; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(MockHttpMessageHandler)); + } + + _requests.Add(request); + + if (_responses.Count == 0) + { + throw new InvalidOperationException("No mock response configured. Call WithResponse() before making requests."); + } + + var mockResponse = _responses.Dequeue(); + + if (mockResponse.Exception != null) + { + throw mockResponse.Exception; + } + + var response = new HttpResponseMessage(mockResponse.StatusCode) + { + Content = new StringContent(mockResponse.Content, System.Text.Encoding.UTF8, mockResponse.ContentType), + RequestMessage = request + }; + + return Task.FromResult(response); + } + + protected override void Dispose(bool disposing) + { + // Don't actually dispose - allow reuse in tests + // The test itself is responsible for cleanup + } + + /// + /// Actually disposes the handler. Call this in test cleanup. + /// + public void ForceDispose() + { + _disposed = true; + base.Dispose(true); + } + + /// + /// Verifies that a request was made to the expected URL. + /// + public bool WasRequestMadeTo(string urlContains) + { + return _requests.Any(r => r.RequestUri?.ToString().Contains(urlContains) == true); + } + + /// + /// Verifies that a request with the expected method was made. + /// + public bool WasRequestMadeWithMethod(HttpMethod method) + { + return _requests.Any(r => r.Method == method); + } + + /// + /// Gets the value of a header from the last request. + /// + public string? GetLastRequestHeader(string headerName) + { + if (LastRequest?.Headers.TryGetValues(headerName, out var values) == true) + { + return values.FirstOrDefault(); + } + return null; + } + + private class MockResponse + { + public HttpStatusCode StatusCode { get; } + public string Content { get; } + public string ContentType { get; } + public Exception? Exception { get; } + + public MockResponse(HttpStatusCode statusCode, string content, string contentType) + { + StatusCode = statusCode; + Content = content; + ContentType = contentType; + } + + public MockResponse(Exception exception) + { + Exception = exception; + StatusCode = HttpStatusCode.InternalServerError; + Content = string.Empty; + ContentType = string.Empty; + } + } +} diff --git a/classlib.tests/Models/AddressTests.cs b/classlib.tests/Models/AddressTests.cs new file mode 100644 index 0000000..60ec426 --- /dev/null +++ b/classlib.tests/Models/AddressTests.cs @@ -0,0 +1,116 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class AddressTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var subnetId = 10; + var ip = "192.168.1.100"; + var isGateway = true; + var description = "Test server"; + var hostname = "server01.example.com"; + var mac = "00:11:22:33:44:55"; + var owner = "admin"; + var tagId = 2; + var deviceId = 5; + var location = "DC1"; + var port = "eth0"; + var note = "Production server"; + var lastSeen = new DateTime(2026, 1, 15, 10, 30, 0); + var excludePing = false; + var ptrIgnore = true; + var ptr = 1; + var firewallObject = "FW_SERVER01"; + var editDate = new DateTime(2026, 1, 10, 8, 0, 0); + var customerId = 100; + var extendedData = new Dictionary { { "custom_field1", "value1" } }; + + // Act + var address = new Address( + id, subnetId, ip, isGateway, description, hostname, mac, owner, + tagId, deviceId, location, port, note, lastSeen, excludePing, + ptrIgnore, ptr, firewallObject, editDate, customerId, extendedData + ); + + // Assert + address.Id.Should().Be(id); + address.SubnetId.Should().Be(subnetId); + address.Ip.Should().Be(ip); + address.IsGateway.Should().Be(isGateway); + address.Description.Should().Be(description); + address.Hostname.Should().Be(hostname); + address.MAC.Should().Be(mac); + address.Owner.Should().Be(owner); + address.TagId.Should().Be(tagId); + address.DeviceId.Should().Be(deviceId); + address.Location.Should().Be(location); + address.Port.Should().Be(port); + address.Note.Should().Be(note); + address.LastSeen.Should().Be(lastSeen); + address.ExcludePing.Should().Be(excludePing); + address.PTRignore.Should().Be(ptrIgnore); + address.PTR.Should().Be(ptr); + address.FirewallAddressObject.Should().Be(firewallObject); + address.EditDate.Should().Be(editDate); + address.CustomerId.Should().Be(customerId); + address.ExtendedData.Should().BeEquivalentTo(extendedData); + } + + [Fact] + public void Constructor_WithNullOptionalFields_SetsNullValues() + { + // Act + var address = new Address( + 1, 10, "10.0.0.1", false, "", "", "", "", + 0, 0, "", "", "", null, false, + false, 0, "", null, 0, null + ); + + // Assert + address.LastSeen.Should().BeNull(); + address.EditDate.Should().BeNull(); + address.ExtendedData.Should().BeNull(); + } + + [Fact] + public void ToString_ReturnsIpAddress() + { + // Arrange + var address = new Address( + 1, 10, "192.168.1.50", false, "Test", "host.local", "", "", + 0, 0, "", "", "", null, false, + false, 0, "", null, 0, null + ); + + // Act + var result = address.ToString(); + + // Assert + result.Should().Be("192.168.1.50"); + } + + [Theory] + [InlineData("10.0.0.1")] + [InlineData("172.16.0.100")] + [InlineData("192.168.255.255")] + [InlineData("2001:db8::1")] + public void ToString_ReturnsCorrectIp_ForVariousAddresses(string expectedIp) + { + // Arrange + var address = new Address( + 1, 1, expectedIp, false, "", "", "", "", + 0, 0, "", "", "", null, false, + false, 0, "", null, 0, null + ); + + // Act & Assert + address.ToString().Should().Be(expectedIp); + } +} diff --git a/classlib.tests/Models/DomainTests.cs b/classlib.tests/Models/DomainTests.cs new file mode 100644 index 0000000..9655e48 --- /dev/null +++ b/classlib.tests/Models/DomainTests.cs @@ -0,0 +1,43 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class DomainTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var name = "Default"; + var description = "Default L2 domain"; + var sections = "1;2;3"; + + // Act + var domain = new Domain(id, name, description, sections); + + // Assert + domain.Id.Should().Be(id); + domain.Name.Should().Be(name); + domain.Description.Should().Be(description); + domain.Sections.Should().Be(sections); + } + + [Theory] + [InlineData("Default")] + [InlineData("Datacenter1")] + [InlineData("Branch_Office")] + public void ToString_ReturnsDomainName(string domainName) + { + // Arrange + var domain = new Domain(1, domainName, "", ""); + + // Act + var result = domain.ToString(); + + // Assert + result.Should().Be(domainName); + } +} diff --git a/classlib.tests/Models/NameserverTests.cs b/classlib.tests/Models/NameserverTests.cs new file mode 100644 index 0000000..570cd9f --- /dev/null +++ b/classlib.tests/Models/NameserverTests.cs @@ -0,0 +1,82 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class NameserverTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var name = "Google DNS"; + var nameServers = "8.8.8.8;8.8.4.4"; + var description = "Google Public DNS"; + var permissions = "{\"3\":\"2\"}"; + var editDate = new DateTime(2026, 1, 10); + + // Act + var nameserver = new Nameserver(id, name, nameServers, description, permissions, editDate); + + // Assert + nameserver.Id.Should().Be(id); + nameserver.Name.Should().Be(name); + nameserver.Description.Should().Be(description); + nameserver.Permissions.Should().Be(permissions); + nameserver.EditDate.Should().Be(editDate); + } + + [Fact] + public void Constructor_ParsesNameservers_BySemicolon() + { + // Arrange + var nameServersString = "8.8.8.8;8.8.4.4;1.1.1.1"; + + // Act + var nameserver = new Nameserver(1, "Test", nameServersString, "", "", null); + + // Assert + nameserver.NameServers.Should().HaveCount(3); + nameserver.NameServers.Should().ContainInOrder("8.8.8.8", "8.8.4.4", "1.1.1.1"); + } + + [Fact] + public void Constructor_WithSingleNameserver_ReturnsArrayWithOneElement() + { + // Arrange + var nameServersString = "8.8.8.8"; + + // Act + var nameserver = new Nameserver(1, "Test", nameServersString, "", "", null); + + // Assert + nameserver.NameServers.Should().HaveCount(1); + nameserver.NameServers[0].Should().Be("8.8.8.8"); + } + + [Fact] + public void Constructor_WithEmptyNameservers_ReturnsArrayWithEmptyString() + { + // Arrange + var nameServersString = ""; + + // Act + var nameserver = new Nameserver(1, "Test", nameServersString, "", "", null); + + // Assert + nameserver.NameServers.Should().HaveCount(1); + nameserver.NameServers[0].Should().BeEmpty(); + } + + [Fact] + public void Constructor_WithNullEditDate_SetsNull() + { + // Act + var nameserver = new Nameserver(1, "Test", "8.8.8.8", "", "", null); + + // Assert + nameserver.EditDate.Should().BeNull(); + } +} diff --git a/classlib.tests/Models/SectionTests.cs b/classlib.tests/Models/SectionTests.cs new file mode 100644 index 0000000..cc83c8d --- /dev/null +++ b/classlib.tests/Models/SectionTests.cs @@ -0,0 +1,77 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class SectionTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var name = "Production"; + var description = "Production networks section"; + var masterSectionId = 0; + var permissions = "{\"3\":\"2\"}"; + var strictMode = true; + var subnetOrdering = "subnet,asc"; + var order = 1; + var editDate = new DateTime(2026, 1, 5); + var showSubnet = true; + var showVlan = true; + var showVRF = false; + var showSupernetOnly = false; + var dnsId = 1; + + // Act + var section = new Section( + id, name, description, masterSectionId, permissions, strictMode, + subnetOrdering, order, editDate, showSubnet, showVlan, showVRF, + showSupernetOnly, dnsId + ); + + // Assert + section.Id.Should().Be(id); + section.Name.Should().Be(name); + section.Description.Should().Be(description); + section.MasterSectionId.Should().Be(masterSectionId); + section.Permissions.Should().Be(permissions); + section.StrictMode.Should().Be(strictMode); + section.SubnetOrdering.Should().Be(subnetOrdering); + section.Order.Should().Be(order); + section.EditDate.Should().Be(editDate); + section.ShowSubnet.Should().Be(showSubnet); + section.ShowVlan.Should().Be(showVlan); + section.ShowVRF.Should().Be(showVRF); + section.ShowSupernetOnly.Should().Be(showSupernetOnly); + section.DNSId.Should().Be(dnsId); + } + + [Theory] + [InlineData("Production")] + [InlineData("Development")] + [InlineData("DMZ")] + public void ToString_ReturnsSectionName(string sectionName) + { + // Arrange + var section = new Section(1, sectionName, "", 0, "", false, "", 0, null, false, false, false, false, 0); + + // Act + var result = section.ToString(); + + // Assert + result.Should().Be(sectionName); + } + + [Fact] + public void Constructor_WithNullEditDate_SetsNull() + { + // Act + var section = new Section(1, "Test", "", 0, "", false, "", 0, null, false, false, false, false, 0); + + // Assert + section.EditDate.Should().BeNull(); + } +} diff --git a/classlib.tests/Models/SessionTests.cs b/classlib.tests/Models/SessionTests.cs new file mode 100644 index 0000000..4d9ebb2 --- /dev/null +++ b/classlib.tests/Models/SessionTests.cs @@ -0,0 +1,92 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class SessionTests +{ + [Fact] + public void Constructor_WithCredentialsAuth_SetsAllProperties() + { + // Arrange + var authType = AuthType.credentials; + var token = "test-token-123"; + var appId = "myApp"; + var url = "https://ipam.example.com"; + var expires = new DateTime(2026, 12, 31, 23, 59, 59); + var credentials = new object(); // Mock credentials + + // Act + var session = new Session(authType, token, appId, url, expires, credentials); + + // Assert + session.AuthType.Should().Be(AuthType.credentials); + session.Token.Should().Be(token); + session.AppID.Should().Be(appId); + session.URL.Should().Be(url); + session.Expires.Should().Be(expires); + session.Credentials.Should().BeSameAs(credentials); + } + + [Fact] + public void Constructor_WithTokenAuth_SetsAllProperties() + { + // Arrange + var authType = AuthType.token; + var token = "static-api-token"; + var appId = "apiApp"; + var url = "https://ipam.test.com"; + + // Act + var session = new Session(authType, token, appId, url, null, null); + + // Assert + session.AuthType.Should().Be(AuthType.token); + session.Token.Should().Be(token); + session.AppID.Should().Be(appId); + session.URL.Should().Be(url); + session.Expires.Should().BeNull(); + session.Credentials.Should().BeNull(); + } + + [Fact] + public void Token_CanBeModified() + { + // Arrange + var session = new Session(AuthType.credentials, "old-token", "app", "https://test.com", null, null); + + // Act + session.Token = "new-token"; + + // Assert + session.Token.Should().Be("new-token"); + } + + [Fact] + public void Expires_CanBeModified() + { + // Arrange + var session = new Session(AuthType.credentials, "token", "app", "https://test.com", null, null); + var newExpiry = new DateTime(2027, 1, 1); + + // Act + session.Expires = newExpiry; + + // Assert + session.Expires.Should().Be(newExpiry); + } + + [Fact] + public void ToString_ReturnsDefaultObjectString() + { + // Arrange + var session = new Session(AuthType.token, "token", "app", "https://test.com", null, null); + + // Act + var result = session.ToString(); + + // Assert + result.Should().Contain("PS.IPAM.Session"); + } +} diff --git a/classlib.tests/Models/SubnetworkTests.cs b/classlib.tests/Models/SubnetworkTests.cs new file mode 100644 index 0000000..eca5166 --- /dev/null +++ b/classlib.tests/Models/SubnetworkTests.cs @@ -0,0 +1,142 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class SubnetworkTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var subnet = "192.168.1.0"; + var mask = 24; + var sectionId = 5; + var description = "Production network"; + var linkedSubnet = "linked-123"; + var firewallObject = "FW_PROD"; + var vrfId = 2; + var masterSubnetId = 0; + var allowRequests = true; + var vlanId = 100; + var showName = true; + var deviceId = 10; + var permissions = "rw"; + var pingSubnet = true; + var discoverSubnet = false; + var resolveDNS = true; + var dnsRecursive = false; + var dnsRecords = true; + var nameserverId = 3; + var scanAgent = false; + var isFolder = false; + var isFull = false; + var isPool = true; + var tagId = 1; + var threshold = 80; + var locationId = 4; + var editDate = new DateTime(2026, 1, 10); + var lastScan = new DateTime(2026, 1, 9); + var lastDiscovery = new DateTime(2026, 1, 8); + var calculation = new { maxhosts = 254 }; + var customFields = new Dictionary { { "custom_env", "prod" } }; + + // Act + var subnetwork = new Subnetwork( + id, subnet, mask, sectionId, description, linkedSubnet, firewallObject, + vrfId, masterSubnetId, allowRequests, vlanId, showName, deviceId, + permissions, pingSubnet, discoverSubnet, resolveDNS, dnsRecursive, + dnsRecords, nameserverId, scanAgent, isFolder, isFull, isPool, + tagId, threshold, locationId, editDate, lastScan, lastDiscovery, + calculation, customFields + ); + + // Assert + subnetwork.Id.Should().Be(id); + subnetwork.Subnet.Should().Be(subnet); + subnetwork.Mask.Should().Be(mask); + subnetwork.SectionId.Should().Be(sectionId); + subnetwork.Description.Should().Be(description); + subnetwork.LinkedSubnet.Should().Be(linkedSubnet); + subnetwork.FirewallAddressObject.Should().Be(firewallObject); + subnetwork.VrfId.Should().Be(vrfId); + subnetwork.MasterSubnetId.Should().Be(masterSubnetId); + subnetwork.AllowRequests.Should().Be(allowRequests); + subnetwork.VlanId.Should().Be(vlanId); + subnetwork.ShowName.Should().Be(showName); + subnetwork.DeviceId.Should().Be(deviceId); + subnetwork.Permissions.Should().Be(permissions); + subnetwork.PingSubnet.Should().Be(pingSubnet); + subnetwork.DiscoverSubnet.Should().Be(discoverSubnet); + subnetwork.ResolveDNS.Should().Be(resolveDNS); + subnetwork.DNSRecursive.Should().Be(dnsRecursive); + subnetwork.DNSRecords.Should().Be(dnsRecords); + subnetwork.NameserverId.Should().Be(nameserverId); + subnetwork.ScanAgent.Should().Be(scanAgent); + subnetwork.IsFolder.Should().Be(isFolder); + subnetwork.IsFull.Should().Be(isFull); + subnetwork.IsPool.Should().Be(isPool); + subnetwork.TagId.Should().Be(tagId); + subnetwork.Threshold.Should().Be(threshold); + subnetwork.LocationId.Should().Be(locationId); + subnetwork.EditDate.Should().Be(editDate); + subnetwork.LastScan.Should().Be(lastScan); + subnetwork.LastDiscovery.Should().Be(lastDiscovery); + subnetwork.Calculation.Should().BeEquivalentTo(calculation); + subnetwork.ExtendedData.Should().BeEquivalentTo(customFields); + } + + [Theory] + [InlineData("192.168.1.0", 24, "192.168.1.0/24")] + [InlineData("10.0.0.0", 8, "10.0.0.0/8")] + [InlineData("172.16.0.0", 16, "172.16.0.0/16")] + [InlineData("192.168.100.0", 30, "192.168.100.0/30")] + public void GetCIDR_ReturnsCidrNotation(string subnet, int mask, string expectedCidr) + { + // Arrange + var subnetwork = CreateSubnetwork(subnet, mask); + + // Act + var result = subnetwork.GetCIDR(); + + // Assert + result.Should().Be(expectedCidr); + } + + [Fact] + public void ToString_ReturnsCidrNotation() + { + // Arrange + var subnetwork = CreateSubnetwork("10.10.0.0", 16); + + // Act + var result = subnetwork.ToString(); + + // Assert + result.Should().Be("10.10.0.0/16"); + } + + [Fact] + public void ToString_EqualsGetCIDR() + { + // Arrange + var subnetwork = CreateSubnetwork("172.20.0.0", 12); + + // Act & Assert + subnetwork.ToString().Should().Be(subnetwork.GetCIDR()); + } + + private static Subnetwork CreateSubnetwork(string subnet, int mask) + { + return new Subnetwork( + 1, subnet, mask, 1, "", "", "", + 0, 0, false, 0, false, 0, + "", false, false, false, false, + false, 0, false, false, false, false, + 0, 0, 0, null, null, null, + new object(), null + ); + } +} diff --git a/classlib.tests/Models/TagTests.cs b/classlib.tests/Models/TagTests.cs new file mode 100644 index 0000000..db345ef --- /dev/null +++ b/classlib.tests/Models/TagTests.cs @@ -0,0 +1,77 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class TagTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var type = "Used"; + var showTag = true; + var bgColor = "#5cb85c"; + var fgColor = "#ffffff"; + var compress = "Yes"; + var locked = "No"; + var updateTag = true; + + // Act + var tag = new Tag(id, type, showTag, bgColor, fgColor, compress, locked, updateTag); + + // Assert + tag.Id.Should().Be(id); + tag.Type.Should().Be(type); + tag.ShowTag.Should().Be(showTag); + tag.BGColor.Should().Be(bgColor); + tag.FGColor.Should().Be(fgColor); + tag.Compress.Should().BeTrue(); + tag.Locked.Should().BeFalse(); + tag.UpdateTag.Should().Be(updateTag); + } + + [Theory] + [InlineData("Yes", true)] + [InlineData("No", false)] + [InlineData("", false)] + [InlineData("yes", false)] // Case sensitive + [InlineData("true", false)] // Only "Yes" is true + public void StringToBool_ConvertsCorrectly(string input, bool expected) + { + // The StringToBool is private, so we test through the constructor + // Using Compress field which uses StringToBool + var tag = new Tag(1, "Test", false, "", "", input, "No", false); + + tag.Compress.Should().Be(expected); + } + + [Theory] + [InlineData("Used")] + [InlineData("Available")] + [InlineData("Reserved")] + [InlineData("DHCP")] + public void ToString_ReturnsTagType(string tagType) + { + // Arrange + var tag = new Tag(1, tagType, false, "", "", "No", "No", false); + + // Act + var result = tag.ToString(); + + // Assert + result.Should().Be(tagType); + } + + [Fact] + public void Locked_WithYes_IsTrue() + { + // Arrange & Act + var tag = new Tag(1, "Test", false, "", "", "No", "Yes", false); + + // Assert + tag.Locked.Should().BeTrue(); + } +} diff --git a/classlib.tests/Models/VlanTests.cs b/classlib.tests/Models/VlanTests.cs new file mode 100644 index 0000000..066956a --- /dev/null +++ b/classlib.tests/Models/VlanTests.cs @@ -0,0 +1,76 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class VlanTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var vlanId = 100; + var domainId = 1; + var name = "Production VLAN"; + var number = 100; + var description = "Production network VLAN"; + var editDate = new DateTime(2026, 1, 15); + var customerId = 50; + var customFields = new Dictionary { { "custom_location", "DC1" } }; + + // Act + var vlan = new Vlan(vlanId, domainId, name, number, description, editDate, customerId, customFields); + + // Assert + vlan.Id.Should().Be(vlanId); + vlan.VlanId.Should().Be(vlanId); + vlan.DomainId.Should().Be(domainId); + vlan.Name.Should().Be(name); + vlan.Number.Should().Be(number); + vlan.Description.Should().Be(description); + vlan.EditDate.Should().Be(editDate); + vlan.CustomerId.Should().Be(customerId); + vlan.ExtendedData.Should().BeEquivalentTo(customFields); + } + + [Fact] + public void Id_And_VlanId_AreSameValue() + { + // Arrange + var vlanId = 200; + + // Act + var vlan = new Vlan(vlanId, 1, "Test", 200, "", null, 0, null); + + // Assert + vlan.Id.Should().Be(vlan.VlanId); + } + + [Theory] + [InlineData(1)] + [InlineData(100)] + [InlineData(4094)] + public void ToString_ReturnsVlanNumber(int vlanNumber) + { + // Arrange + var vlan = new Vlan(1, 1, "Test", vlanNumber, "", null, 0, null); + + // Act + var result = vlan.ToString(); + + // Assert + result.Should().Be(vlanNumber.ToString()); + } + + [Fact] + public void Constructor_WithNullOptionalFields_SetsNullValues() + { + // Act + var vlan = new Vlan(1, 1, "Test", 10, "", null, 0, null); + + // Assert + vlan.EditDate.Should().BeNull(); + vlan.ExtendedData.Should().BeNull(); + } +} diff --git a/classlib.tests/Models/VrfTests.cs b/classlib.tests/Models/VrfTests.cs new file mode 100644 index 0000000..d494dbb --- /dev/null +++ b/classlib.tests/Models/VrfTests.cs @@ -0,0 +1,60 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class VrfTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var name = "VRF_PROD"; + var rd = "65000:100"; + var description = "Production VRF"; + var sections = "1;2;3"; + var editDate = new DateTime(2026, 1, 10); + var customFields = new Dictionary { { "custom_tenant", "CustomerA" } }; + + // Act + var vrf = new Vrf(id, name, rd, description, sections, editDate, customFields); + + // Assert + vrf.Id.Should().Be(id); + vrf.Name.Should().Be(name); + vrf.RouteDistinguisher.Should().Be(rd); + vrf.Description.Should().Be(description); + vrf.Sections.Should().Be(sections); + vrf.EditDate.Should().Be(editDate); + vrf.ExtendedData.Should().BeEquivalentTo(customFields); + } + + [Theory] + [InlineData("VRF_DEFAULT")] + [InlineData("Production_VRF")] + [InlineData("CUSTOMER_A")] + public void ToString_ReturnsVrfName(string vrfName) + { + // Arrange + var vrf = new Vrf(1, vrfName, "65000:1", "", "", null, null); + + // Act + var result = vrf.ToString(); + + // Assert + result.Should().Be(vrfName); + } + + [Fact] + public void Constructor_WithNullOptionalFields_SetsNullValues() + { + // Act + var vrf = new Vrf(1, "Test", "65000:1", "", "", null, null); + + // Assert + vrf.EditDate.Should().BeNull(); + vrf.ExtendedData.Should().BeNull(); + } +} diff --git a/classlib.tests/classlib.tests.csproj b/classlib.tests/classlib.tests.csproj new file mode 100644 index 0000000..0c4f99c --- /dev/null +++ b/classlib.tests/classlib.tests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + latest + false + true + PS.IPAM.Tests + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + +