From 010cce0fd8ee17268a6f4a523f9400f091ed05f0 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 19 Jan 2026 14:44:52 +0300 Subject: [PATCH] Refactored subnet object methods and enhanced related documentation --- classlib.tests/Helpers/RequestHelperTests.cs | 503 ++++++++++++++++++ classlib.tests/Helpers/SessionManagerTests.cs | 193 +++++++ classlib.tests/TestCollections.cs | 32 ++ classlib/Helpers/RequestHelper.cs | 374 +++++++++++++ 4 files changed, 1102 insertions(+) create mode 100644 classlib.tests/Helpers/RequestHelperTests.cs create mode 100644 classlib.tests/Helpers/SessionManagerTests.cs create mode 100644 classlib.tests/TestCollections.cs create mode 100644 classlib/Helpers/RequestHelper.cs diff --git a/classlib.tests/Helpers/RequestHelperTests.cs b/classlib.tests/Helpers/RequestHelperTests.cs new file mode 100644 index 0000000..90616a0 --- /dev/null +++ b/classlib.tests/Helpers/RequestHelperTests.cs @@ -0,0 +1,503 @@ +namespace PS.IPAM.Tests.Helpers; + +using System.Net; +using FluentAssertions; +using PS.IPAM; +using PS.IPAM.Helpers; +using PS.IPAM.Tests.Mocks; +using Xunit; + +[Collection("Sequential")] +public class RequestHelperTests : IDisposable +{ + private MockHttpMessageHandler? _mockHandler; + + public RequestHelperTests() + { + // Clean state before each test + SessionManager.CloseSession(); + RequestHelper.TestHttpHandler = null; + } + + public void Dispose() + { + // Clean up after each test + SessionManager.CloseSession(); + RequestHelper.TestHttpHandler = null; + _mockHandler?.ForceDispose(); + } + + private void SetupSession(AuthType authType = AuthType.token) + { + if (authType == AuthType.token) + { + SessionManager.CreateSessionWithToken("https://ipam.example.com", "testapp", "test-token"); + } + else + { + // For credentials auth, we need to set up a session manually with future expiry + var session = new Session( + AuthType.credentials, + "cred-token", + "testapp", + "https://ipam.example.com", + DateTime.Now.AddHours(1), + null + ); + SessionManager.CurrentSession = session; + } + } + + private MockHttpMessageHandler SetupMockHandler() + { + _mockHandler = new MockHttpMessageHandler(); + RequestHelper.TestHttpHandler = _mockHandler; + return _mockHandler; + } + + [Fact] + public async Task InvokeRequest_WithNoSession_ThrowsException() + { + // Arrange - no session set up + + // Act + var action = async () => await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + await action.Should().ThrowAsync().WithMessage("No session available!"); + } + + [Fact] + public async Task InvokeRequest_WithValidSession_BuildsCorrectUri() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{\"id\":1,\"ip\":\"192.168.1.1\"}"); + + // Act + await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "1" }); + + // Assert + handler.LastRequest.Should().NotBeNull(); + handler.LastRequest!.RequestUri!.ToString().Should().Be("https://ipam.example.com/api/testapp/addresses/1/"); + } + + [Fact] + public async Task InvokeRequest_WithSubController_BuildsCorrectUri() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("[]"); + + // Act + await RequestHelper.InvokeRequest("GET", controllers.subnets, null, subcontrollers.tags, null, new[] { "1" }); + + // Assert + handler.LastRequest!.RequestUri!.ToString().Should().Contain("/subnets/tags/1/"); + } + + [Fact] + public async Task InvokeRequest_WithTokenAuth_AddsPhpipamTokenHeader() + { + // Arrange + SetupSession(AuthType.token); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{}"); + + // Act + await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + handler.GetLastRequestHeader("phpipam-token").Should().Be("test-token"); + } + + [Fact] + public async Task InvokeRequest_WithCredentialsAuth_AddsTokenHeader() + { + // Arrange + SetupSession(AuthType.credentials); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{}"); + + // Act + await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + handler.GetLastRequestHeader("token").Should().Be("cred-token"); + } + + [Fact] + public async Task InvokeRequest_GetMethod_UsesHttpGet() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{}"); + + // Act + await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + handler.LastRequest!.Method.Should().Be(HttpMethod.Get); + } + + [Fact] + public async Task InvokeRequest_PostMethod_UsesHttpPost() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{\"id\":1}"); + + // Act + await RequestHelper.InvokeRequest("POST", controllers.addresses, null, null, new { ip = "10.0.0.1" }); + + // Assert + handler.LastRequest!.Method.Should().Be(HttpMethod.Post); + } + + [Fact] + public async Task InvokeRequest_PatchMethod_UsesHttpPatch() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{\"id\":1}"); + + // Act + await RequestHelper.InvokeRequest("PATCH", controllers.addresses, null, null, new { description = "updated" }, new[] { "1" }); + + // Assert + handler.LastRequest!.Method.Should().Be(new HttpMethod("PATCH")); + } + + [Fact] + public async Task InvokeRequest_DeleteMethod_UsesHttpDelete() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{}"); + + // Act + await RequestHelper.InvokeRequest("DELETE", controllers.addresses, null, null, null, new[] { "1" }); + + // Assert + handler.LastRequest!.Method.Should().Be(HttpMethod.Delete); + } + + [Fact] + public async Task InvokeRequest_With404Response_ReturnsNull() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithNotFoundResponse(); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "999" }); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public async Task InvokeRequest_WithErrorResponse_ThrowsHttpRequestException() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithErrorResponse(HttpStatusCode.InternalServerError, "Server error"); + + // Act + var action = async () => await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task InvokeRequest_WithAddressType_ReturnsAddressObject() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + var addressJson = @"{ + ""id"": 1, + ""subnetId"": 10, + ""ip"": ""192.168.1.100"", + ""is_gateway"": false, + ""description"": ""Test server"", + ""hostname"": ""server01"", + ""mac"": ""00:11:22:33:44:55"", + ""owner"": ""admin"", + ""tag"": 2, + ""deviceId"": 0, + ""location"": """", + ""port"": """", + ""note"": """", + ""lastSeen"": null, + ""excludePing"": false, + ""PTRignore"": false, + ""PTR"": 0, + ""firewallAddressObject"": """", + ""editDate"": null, + ""customer_id"": 0 + }"; + handler.WithSuccessResponse(addressJson); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "1" }); + + // Assert + result.Should().BeOfType
(); + var address = (Address)result!; + address.Id.Should().Be(1); + address.Ip.Should().Be("192.168.1.100"); + address.Hostname.Should().Be("server01"); + } + + [Fact] + public async Task InvokeRequest_WithAddressArray_ReturnsListOfAddresses() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + var addressArrayJson = @"[ + {""id"": 1, ""subnetId"": 10, ""ip"": ""192.168.1.1"", ""hostname"": ""host1""}, + {""id"": 2, ""subnetId"": 10, ""ip"": ""192.168.1.2"", ""hostname"": ""host2""} + ]"; + handler.WithSuccessResponse(addressArrayJson); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.subnets, types.Address, null, null, new[] { "10", "addresses" }); + + // Assert + result.Should().BeAssignableTo>(); + var addresses = ((IEnumerable)result!).ToList(); + addresses.Should().HaveCount(2); + addresses[0].Should().BeOfType
(); + ((Address)addresses[0]).Ip.Should().Be("192.168.1.1"); + } + + [Fact] + public async Task InvokeRequest_WithVlanType_ReturnsVlanObject() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + var vlanJson = @"{ + ""vlanId"": 100, + ""domainId"": 1, + ""name"": ""Production"", + ""number"": 100, + ""description"": ""Production VLAN"", + ""editDate"": null, + ""customer_id"": 0 + }"; + handler.WithSuccessResponse(vlanJson); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.vlan, types.Vlan, null, null, new[] { "100" }); + + // Assert + result.Should().BeOfType(); + var vlan = (Vlan)result!; + vlan.Name.Should().Be("Production"); + vlan.Number.Should().Be(100); + } + + [Fact] + public async Task InvokeRequest_WithSubnetworkType_ReturnsSubnetworkObject() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + var subnetJson = @"{ + ""id"": 1, + ""subnet"": ""192.168.1.0"", + ""mask"": 24, + ""sectionId"": 1, + ""description"": ""Test subnet"", + ""linked_subnet"": """", + ""firewallAddressObject"": """", + ""vrfId"": 0, + ""masterSubnetId"": 0, + ""allowRequests"": false, + ""vlanId"": 100, + ""showName"": false, + ""deviceId"": 0, + ""permissions"": """", + ""pingSubnet"": false, + ""discoverSubnet"": false, + ""resolveDNS"": false, + ""DNSrecursive"": false, + ""DNSrecords"": false, + ""nameserverId"": 0, + ""scanAgent"": false, + ""isFolder"": false, + ""isFull"": false, + ""isPool"": false, + ""state"": 1, + ""threshold"": 0, + ""location"": 0, + ""editDate"": null, + ""lastScan"": null, + ""lastDiscovery"": null, + ""calculation"": {} + }"; + handler.WithSuccessResponse(subnetJson); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.subnets, types.Subnetwork, null, null, new[] { "1" }); + + // Assert + result.Should().BeOfType(); + var subnet = (Subnetwork)result!; + subnet.Subnet.Should().Be("192.168.1.0"); + subnet.Mask.Should().Be(24); + subnet.GetCIDR().Should().Be("192.168.1.0/24"); + } + + [Fact] + public async Task InvokeRequest_WithSectionType_ReturnsSectionObject() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + var sectionJson = @"{ + ""id"": 1, + ""name"": ""Production"", + ""description"": ""Production section"", + ""masterSection"": 0, + ""permissions"": """", + ""strictMode"": false, + ""subnetOrdering"": ""default"", + ""order"": 1, + ""editDate"": null, + ""showSubnet"": true, + ""showVlan"": true, + ""showVRF"": false, + ""showSupernetOnly"": false, + ""DNS"": 0 + }"; + handler.WithSuccessResponse(sectionJson); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.sections, types.Section, null, null, new[] { "1" }); + + // Assert + result.Should().BeOfType
(); + var section = (Section)result!; + section.Name.Should().Be("Production"); + } + + [Fact] + public async Task InvokeRequest_WithCustomFields_ParsesExtendedData() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + var addressJson = @"{ + ""id"": 1, + ""subnetId"": 10, + ""ip"": ""192.168.1.100"", + ""hostname"": ""server01"", + ""custom_environment"": ""production"", + ""custom_owner"": ""team-a"" + }"; + handler.WithSuccessResponse(addressJson); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "1" }); + + // Assert + var address = (Address)result!; + address.ExtendedData.Should().NotBeNull(); + var extendedData = (Dictionary)address.ExtendedData!; + extendedData.Should().ContainKey("custom_environment"); + extendedData.Should().ContainKey("custom_owner"); + } + + [Fact] + public async Task InvokeRequest_WithNoType_ReturnsDynamicData() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse(@"{""some"": ""data""}"); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.tools); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public async Task InvokeRequest_PostWithParameters_SerializesJsonBody() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse(@"{""id"": 1}"); + + var parameters = new { ip = "10.0.0.1", subnetId = 5, description = "New address" }; + + // Act + await RequestHelper.InvokeRequest("POST", controllers.addresses, null, null, parameters); + + // Assert + handler.LastRequest!.Content.Should().NotBeNull(); + var content = await handler.LastRequest.Content!.ReadAsStringAsync(); + content.Should().Contain("10.0.0.1"); + content.Should().Contain("subnetId"); + } + + [Fact] + public async Task InvokeRequest_SetsAcceptJsonHeader() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{}"); + + // Act + await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + handler.LastRequest!.Headers.Accept.Should().Contain(h => h.MediaType == "application/json"); + } + + [Fact] + public async Task InvokeRequest_WithEmptyResponse_ReturnsNull() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithResponse(HttpStatusCode.OK, "", "application/json"); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public async Task InvokeRequest_WithNullDataInResponse_ReturnsNull() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithJsonResponse(@"{""code"": 200, ""success"": true, ""data"": null}"); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address); + + // Assert + result.Should().BeNull(); + } +} diff --git a/classlib.tests/Helpers/SessionManagerTests.cs b/classlib.tests/Helpers/SessionManagerTests.cs new file mode 100644 index 0000000..d521863 --- /dev/null +++ b/classlib.tests/Helpers/SessionManagerTests.cs @@ -0,0 +1,193 @@ +namespace PS.IPAM.Tests.Helpers; + +using FluentAssertions; +using PS.IPAM; +using PS.IPAM.Helpers; +using Xunit; + +[Collection("Sequential")] +public class SessionManagerTests : IDisposable +{ + public SessionManagerTests() + { + // Ensure clean state before each test + SessionManager.CloseSession(); + } + + public void Dispose() + { + // Clean up after each test + SessionManager.CloseSession(); + } + + [Fact] + public void TestSession_WhenNoSession_ReturnsNoToken() + { + // Arrange + SessionManager.CurrentSession = null; + + // Act + var result = SessionManager.TestSession(); + + // Assert + result.Should().Be("NoToken"); + } + + [Fact] + public void TestSession_WhenSessionWithNullExpires_ReturnsValid() + { + // Arrange + var session = new Session(AuthType.token, "test-token", "app", "https://test.com", null, null); + SessionManager.CurrentSession = session; + + // Act + var result = SessionManager.TestSession(); + + // Assert + result.Should().Be("Valid"); + } + + [Fact] + public void TestSession_WhenSessionNotExpired_ReturnsValid() + { + // Arrange + var futureExpiry = DateTime.Now.AddHours(1); + var session = new Session(AuthType.credentials, "test-token", "app", "https://test.com", futureExpiry, null); + SessionManager.CurrentSession = session; + + // Act + var result = SessionManager.TestSession(); + + // Assert + result.Should().Be("Valid"); + } + + [Fact] + public void TestSession_WhenSessionExpired_ReturnsExpired() + { + // Arrange + var pastExpiry = DateTime.Now.AddHours(-1); + var session = new Session(AuthType.credentials, "test-token", "app", "https://test.com", pastExpiry, null); + SessionManager.CurrentSession = session; + + // Act + var result = SessionManager.TestSession(); + + // Assert + result.Should().Be("Expired"); + } + + [Fact] + public void CreateSessionWithToken_CreatesValidSession() + { + // Arrange + var url = "https://ipam.example.com"; + var appId = "myApp"; + var token = "static-api-token"; + + // Act + var session = SessionManager.CreateSessionWithToken(url, appId, token); + + // Assert + session.Should().NotBeNull(); + session.URL.Should().Be(url); + session.AppID.Should().Be(appId); + session.Token.Should().Be(token); + session.AuthType.Should().Be(AuthType.token); + session.Expires.Should().BeNull(); + session.Credentials.Should().BeNull(); + } + + [Fact] + public void CreateSessionWithToken_SetsCurrentSession() + { + // Arrange + var url = "https://ipam.example.com"; + var appId = "myApp"; + var token = "api-token"; + + // Act + var session = SessionManager.CreateSessionWithToken(url, appId, token); + + // Assert + SessionManager.CurrentSession.Should().BeSameAs(session); + } + + [Fact] + public void CloseSession_ClearsCurrentSession() + { + // Arrange + SessionManager.CreateSessionWithToken("https://test.com", "app", "token"); + SessionManager.CurrentSession.Should().NotBeNull(); + + // Act + SessionManager.CloseSession(); + + // Assert + SessionManager.CurrentSession.Should().BeNull(); + } + + [Fact] + public void CloseSession_WhenNoSession_DoesNotThrow() + { + // Arrange + SessionManager.CurrentSession = null; + + // Act + var action = () => SessionManager.CloseSession(); + + // Assert + action.Should().NotThrow(); + } + + [Fact] + public void CurrentSession_CanBeSetDirectly() + { + // Arrange + var session = new Session(AuthType.token, "token", "app", "https://test.com", null, null); + + // Act + SessionManager.CurrentSession = session; + + // Assert + SessionManager.CurrentSession.Should().BeSameAs(session); + } + + [Fact] + public void CreateHttpClient_WithoutIgnoreSsl_ReturnsHttpClient() + { + // Act + using var client = SessionManager.CreateHttpClient(false); + + // Assert + client.Should().NotBeNull(); + client.Should().BeOfType(); + } + + [Fact] + public void CreateHttpClient_WithIgnoreSsl_ReturnsHttpClient() + { + // Act + using var client = SessionManager.CreateHttpClient(true); + + // Assert + client.Should().NotBeNull(); + client.Should().BeOfType(); + } + + [Fact] + public void CreateSessionWithToken_ReplacesExistingSession() + { + // Arrange + SessionManager.CreateSessionWithToken("https://old.com", "oldApp", "oldToken"); + + // Act + var newSession = SessionManager.CreateSessionWithToken("https://new.com", "newApp", "newToken"); + + // Assert + SessionManager.CurrentSession.Should().BeSameAs(newSession); + SessionManager.CurrentSession!.URL.Should().Be("https://new.com"); + SessionManager.CurrentSession.AppID.Should().Be("newApp"); + SessionManager.CurrentSession.Token.Should().Be("newToken"); + } +} diff --git a/classlib.tests/TestCollections.cs b/classlib.tests/TestCollections.cs new file mode 100644 index 0000000..3e4ff33 --- /dev/null +++ b/classlib.tests/TestCollections.cs @@ -0,0 +1,32 @@ +namespace PS.IPAM.Tests; + +using Xunit; + +/// +/// Collection definition for tests that share static state (SessionManager, RequestHelper.TestHttpHandler). +/// Tests in this collection will run sequentially, not in parallel. +/// +[CollectionDefinition("Sequential")] +public class SequentialCollection : ICollectionFixture +{ +} + +/// +/// Fixture for sequential test collection. +/// +public class SequentialTestFixture : IDisposable +{ + public SequentialTestFixture() + { + // Clean up before tests + PS.IPAM.Helpers.SessionManager.CloseSession(); + PS.IPAM.Helpers.RequestHelper.TestHttpHandler = null; + } + + public void Dispose() + { + // Clean up after tests + PS.IPAM.Helpers.SessionManager.CloseSession(); + PS.IPAM.Helpers.RequestHelper.TestHttpHandler = null; + } +} diff --git a/classlib/Helpers/RequestHelper.cs b/classlib/Helpers/RequestHelper.cs new file mode 100644 index 0000000..8ee8178 --- /dev/null +++ b/classlib/Helpers/RequestHelper.cs @@ -0,0 +1,374 @@ +namespace PS.IPAM.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PS.IPAM; + +public static class RequestHelper +{ + // Handler for testing - allows injecting a mock HTTP handler + public static HttpMessageHandler? TestHttpHandler { get; set; } + + public static async Task InvokeRequest( + string method, + controllers controller, + types? type = null, + subcontrollers? subController = null, + object? parameters = null, + string[]? identifiers = null, + bool ignoreSsl = false) + { + var tokenStatus = SessionManager.TestSession(); + if (tokenStatus == "NoToken") + { + throw new Exception("No session available!"); + } + + if (tokenStatus == "Expired") + { + await UpdateSession(); + } + + var session = SessionManager.CurrentSession; + if (session == null) + { + throw new Exception("No session available!"); + } + + var uri = $"{session.URL}/api/{session.AppID}/{controller}"; + if (subController != null) + { + uri += $"/{subController}"; + } + if (identifiers != null && identifiers.Length > 0) + { + uri += $"/{string.Join("/", identifiers)}/"; + } + + using var client = SessionManager.CreateHttpClient(ignoreSsl, TestHttpHandler); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + switch (session.AuthType) + { + case AuthType.credentials: + client.DefaultRequestHeaders.Add("token", session.Token); + break; + case AuthType.token: + client.DefaultRequestHeaders.Add("phpipam-token", session.Token); + break; + } + + HttpResponseMessage? response = null; + try + { + if (method == "GET") + { + response = await client.GetAsync(uri); + } + else if (method == "POST") + { + var jsonContent = parameters != null ? JsonConvert.SerializeObject(parameters) : "{}"; + var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + response = await client.PostAsync(uri, content); + } + else if (method == "PATCH") + { + var jsonContent = parameters != null ? JsonConvert.SerializeObject(parameters) : "{}"; + var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + var request = new HttpRequestMessage(new HttpMethod("PATCH"), uri) + { + Content = content + }; + response = await client.SendAsync(request); + } + else if (method == "DELETE") + { + response = await client.DeleteAsync(uri); + } + + if (response == null) + { + return null; + } + + var responseContent = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return null; + } + throw new HttpRequestException($"Request failed with status {response.StatusCode}"); + } + + if (string.IsNullOrEmpty(responseContent)) + { + return null; + } + + var jsonResponse = JsonConvert.DeserializeObject(responseContent); + if (jsonResponse == null) + { + return null; + } + + if (type.HasValue) + { + return ConvertToTypedObjects(jsonResponse, type.Value); + } + + return jsonResponse.data; + } + catch (HttpRequestException ex) + { + if (ex.Message.Contains("404")) + { + return null; + } + throw; + } + } + + private static object? ConvertToTypedObjects(dynamic jsonResponse, types type) + { + if (jsonResponse?.data == null) + { + return null; + } + + var data = jsonResponse.data; + if (data is JArray array) + { + return array.Select(item => ConvertSingleObject(item, type)).ToList(); + } + else + { + return ConvertSingleObject(data, type); + } + } + + private static object? ConvertSingleObject(dynamic item, types type) + { + var jobject = item as JObject; + if (jobject == null) + { + return null; + } + + var customFields = new Dictionary(); + foreach (var prop in jobject.Properties()) + { + if (prop.Name.StartsWith("custom_")) + { + customFields[prop.Name] = prop.Value?.ToObject() ?? new object(); + } + } + + switch (type) + { + case types.Address: + return CreateAddress(jobject, customFields); + case types.Vlan: + return CreateVlan(jobject, customFields); + case types.Subnetwork: + return CreateSubnetwork(jobject, customFields); + case types.Vrf: + return CreateVrf(jobject, customFields); + case types.Section: + return CreateSection(jobject); + case types.Tag: + return CreateTag(jobject); + case types.Nameserver: + return CreateNameserver(jobject); + case types.Domain: + return CreateDomain(jobject); + default: + return jobject.ToObject(); + } + } + + private static Address CreateAddress(JObject jobject, Dictionary customFields) + { + return new Address( + jobject["id"]?.ToObject() ?? 0, + jobject["subnetId"]?.ToObject() ?? 0, + jobject["ip"]?.ToString() ?? "", + jobject["is_gateway"]?.ToObject() ?? false, + jobject["description"]?.ToString() ?? "", + jobject["hostname"]?.ToString() ?? "", + jobject["mac"]?.ToString() ?? "", + jobject["owner"]?.ToString() ?? "", + jobject["tag"]?.ToObject() ?? 0, + jobject["deviceId"]?.ToObject() ?? 0, + jobject["location"]?.ToString() ?? "", + jobject["port"]?.ToString() ?? "", + jobject["note"]?.ToString() ?? "", + jobject["lastSeen"]?.ToObject(), + jobject["excludePing"]?.ToObject() ?? false, + jobject["PTRignore"]?.ToObject() ?? false, + jobject["PTR"]?.ToObject() ?? 0, + jobject["firewallAddressObject"]?.ToString() ?? "", + jobject["editDate"]?.ToObject(), + jobject["customer_id"]?.ToObject() ?? 0, + customFields.Count > 0 ? customFields : null + ); + } + + private static Vlan CreateVlan(JObject jobject, Dictionary customFields) + { + return new Vlan( + jobject["vlanId"]?.ToObject() ?? 0, + jobject["domainId"]?.ToObject() ?? 0, + jobject["name"]?.ToString() ?? "", + jobject["number"]?.ToObject() ?? 0, + jobject["description"]?.ToString() ?? "", + jobject["editDate"]?.ToObject(), + jobject["customer_id"]?.ToObject() ?? 0, + customFields.Count > 0 ? customFields : null + ); + } + + private static Subnetwork CreateSubnetwork(JObject jobject, Dictionary customFields) + { + var props = jobject.Properties().ToList(); + return new Subnetwork( + jobject["id"]?.ToObject() ?? 0, + jobject["subnet"]?.ToString() ?? "", + jobject["mask"]?.ToObject() ?? 0, + jobject["sectionId"]?.ToObject() ?? 0, + jobject["description"]?.ToString() ?? "", + jobject["linked_subnet"]?.ToString() ?? "", + jobject["firewallAddressObject"]?.ToString() ?? "", + jobject["vrfId"]?.ToObject() ?? 0, + jobject["masterSubnetId"]?.ToObject() ?? 0, + jobject["allowRequests"]?.ToObject() ?? false, + jobject["vlanId"]?.ToObject() ?? 0, + jobject["showName"]?.ToObject() ?? false, + jobject["deviceId"]?.ToObject() ?? 0, + jobject["permissions"]?.ToString() ?? "", + jobject["pingSubnet"]?.ToObject() ?? false, + jobject["discoverSubnet"]?.ToObject() ?? false, + jobject["resolveDNS"]?.ToObject() ?? false, + jobject["DNSrecursive"]?.ToObject() ?? false, + jobject["DNSrecords"]?.ToObject() ?? false, + jobject["nameserverId"]?.ToObject() ?? 0, + jobject["scanAgent"]?.ToObject() ?? false, + jobject["isFolder"]?.ToObject() ?? false, + jobject["isFull"]?.ToObject() ?? false, + jobject["isPool"]?.ToObject() ?? false, + jobject["state"]?.ToObject() ?? 0, + jobject["threshold"]?.ToObject() ?? 0, + jobject["location"]?.ToObject() ?? 0, + jobject["editDate"]?.ToObject(), + jobject["lastScan"]?.ToObject(), + jobject["lastDiscovery"]?.ToObject(), + jobject["calculation"]?.ToObject() ?? new object(), + customFields.Count > 0 ? customFields : null + ); + } + + private static Vrf CreateVrf(JObject jobject, Dictionary customFields) + { + return new Vrf( + jobject["id"]?.ToObject() ?? 0, + jobject["name"]?.ToString() ?? "", + jobject["rd"]?.ToString() ?? "", + jobject["description"]?.ToString() ?? "", + jobject["sections"]?.ToString() ?? "", + jobject["editDate"]?.ToObject(), + customFields.Count > 0 ? customFields : null + ); + } + + private static Section CreateSection(JObject jobject) + { + return new Section( + jobject["id"]?.ToObject() ?? 0, + jobject["name"]?.ToString() ?? "", + jobject["description"]?.ToString() ?? "", + jobject["masterSection"]?.ToObject() ?? 0, + jobject["permissions"]?.ToString() ?? "", + jobject["strictMode"]?.ToObject() ?? false, + jobject["subnetOrdering"]?.ToString() ?? "", + jobject["order"]?.ToObject() ?? 0, + jobject["editDate"]?.ToObject(), + jobject["showSubnet"]?.ToObject() ?? false, + jobject["showVlan"]?.ToObject() ?? false, + jobject["showVRF"]?.ToObject() ?? false, + jobject["showSupernetOnly"]?.ToObject() ?? false, + jobject["DNS"]?.ToObject() ?? 0 + ); + } + + private static Tag CreateTag(JObject jobject) + { + return new Tag( + jobject["id"]?.ToObject() ?? 0, + jobject["type"]?.ToString() ?? "", + jobject["showtag"]?.ToObject() ?? false, + jobject["bgcolor"]?.ToString() ?? "", + jobject["fgcolor"]?.ToString() ?? "", + jobject["compress"]?.ToString() ?? "", + jobject["locked"]?.ToString() ?? "", + jobject["updateTag"]?.ToObject() ?? false + ); + } + + private static Nameserver CreateNameserver(JObject jobject) + { + return new Nameserver( + jobject["id"]?.ToObject() ?? 0, + jobject["name"]?.ToString() ?? "", + jobject["nameservers"]?.ToString() ?? "", + jobject["description"]?.ToString() ?? "", + jobject["permissions"]?.ToString() ?? "", + jobject["editDate"]?.ToObject() + ); + } + + private static Domain CreateDomain(JObject jobject) + { + return new Domain( + jobject["id"]?.ToObject() ?? 0, + jobject["name"]?.ToString() ?? "", + jobject["description"]?.ToString() ?? "", + jobject["sections"]?.ToString() ?? "" + ); + } + + private static async Task UpdateSession() + { + var session = SessionManager.CurrentSession; + if (session == null) + { + throw new Exception("No session available!"); + } + + var tokenStatus = SessionManager.TestSession(); + if (tokenStatus == "Valid") + { + // Just refresh the token + var result = await InvokeRequest("PATCH", controllers.user, null, null, null, null); + // Token refresh doesn't return expires in the same format, so we'll skip updating expires + return; + } + + if (tokenStatus == "Expired" && session.Credentials is PSCredential creds) + { + await SessionManager.CreateSessionWithCredentials( + session.URL, + session.AppID, + creds, + false + ); + } + } +}