diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml
new file mode 100644
index 0000000..1e86b2a
--- /dev/null
+++ b/.gitea/workflows/ci.yaml
@@ -0,0 +1,121 @@
+name: CI/CD Pipeline
+
+on:
+ push:
+ branches:
+ - main
+ - master
+ - develop
+ tags:
+ - 'v*'
+ pull_request:
+ branches:
+ - main
+ - master
+
+jobs:
+ build:
+ name: Build and Test
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build class library
+ run: dotnet build classlib/classlib.csproj --configuration Release --no-restore
+
+ - name: Build test project
+ run: dotnet build classlib.tests/classlib.tests.csproj --configuration Release --no-restore
+
+ - name: Run tests
+ run: dotnet test classlib.tests/classlib.tests.csproj --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" --collect:"XPlat Code Coverage"
+
+ - name: Upload test results
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: test-results
+ path: classlib.tests/TestResults/
+ retention-days: 30
+
+ package:
+ name: Package Module
+ runs-on: ubuntu-latest
+ needs: build
+ if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v'))
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Build Release
+ run: dotnet build classlib/classlib.csproj --configuration Release
+
+ - name: Create module package
+ run: |
+ mkdir -p output/ps.ipam
+
+ # Copy compiled DLL
+ cp classlib/bin/Release/netstandard2.1/ps.ipam.dll output/ps.ipam/
+
+ # Copy module manifest and related files
+ cp ps.ipam.psd1 output/ps.ipam/
+ cp ps.ipam.psm1 output/ps.ipam/
+ cp LICENSE output/ps.ipam/
+ cp README.md output/ps.ipam/
+
+ # Copy types directory
+ cp -r types output/ps.ipam/
+
+ # Copy functions directory
+ cp -r functions output/ps.ipam/
+
+ # Copy images directory
+ cp -r images output/ps.ipam/
+
+ - name: Upload module artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ps.ipam-module
+ path: output/ps.ipam/
+ retention-days: 90
+
+ release:
+ name: Create Release
+ runs-on: ubuntu-latest
+ needs: package
+ if: startsWith(github.ref, 'refs/tags/v')
+ steps:
+ - name: Download module artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: ps.ipam-module
+ path: ps.ipam
+
+ - name: Create release archive
+ run: |
+ zip -r ps.ipam-${{ github.ref_name }}.zip ps.ipam/
+ tar -czvf ps.ipam-${{ github.ref_name }}.tar.gz ps.ipam/
+
+ - name: Create Gitea Release
+ uses: actions/gitea-release-action@v1
+ with:
+ token: ${{ secrets.TOKEN }}
+ files: |
+ ps.ipam-${{ github.ref_name }}.zip
+ ps.ipam-${{ github.ref_name }}.tar.gz
+ title: Release ${{ github.ref_name }}
+ draft: false
+ prerelease: ${{ contains(github.ref_name, '-') }}
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/Cmdlets/GetAddressCmdletTests.cs b/classlib.tests/Cmdlets/GetAddressCmdletTests.cs
new file mode 100644
index 0000000..8ca88e5
--- /dev/null
+++ b/classlib.tests/Cmdlets/GetAddressCmdletTests.cs
@@ -0,0 +1,323 @@
+namespace PS.IPAM.Tests.Cmdlets;
+
+using System.Net;
+using FluentAssertions;
+using PS.IPAM;
+using PS.IPAM.Cmdlets;
+using PS.IPAM.Helpers;
+using PS.IPAM.Tests.Mocks;
+using Xunit;
+
+///
+/// Tests for the GetAddressCmdlet.
+/// Note: Full cmdlet testing with parameter sets requires a PowerShell runspace.
+/// These tests focus on verifying the cmdlet structure and the underlying RequestHelper functionality.
+///
+[Collection("Sequential")]
+public class GetAddressCmdletTests : IDisposable
+{
+ private MockHttpMessageHandler? _mockHandler;
+
+ public GetAddressCmdletTests()
+ {
+ // 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()
+ {
+ SessionManager.CreateSessionWithToken("https://ipam.example.com", "testapp", "test-token");
+ }
+
+ private MockHttpMessageHandler SetupMockHandler()
+ {
+ _mockHandler = new MockHttpMessageHandler();
+ RequestHelper.TestHttpHandler = _mockHandler;
+ return _mockHandler;
+ }
+
+ [Fact]
+ public void GetAddressCmdlet_Exists()
+ {
+ // Verify the cmdlet class exists and can be instantiated
+ var cmdlet = new GetAddressCmdlet();
+ cmdlet.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void GetAddressCmdlet_IdProperty_Exists()
+ {
+ var cmdlet = new GetAddressCmdlet();
+ cmdlet.Id = 42;
+ cmdlet.Id.Should().Be(42);
+ }
+
+ [Fact]
+ public void GetAddressCmdlet_IPProperty_Exists()
+ {
+ var cmdlet = new GetAddressCmdlet();
+ cmdlet.IP = IPAddress.Parse("192.168.1.100");
+ cmdlet.IP.Should().Be(IPAddress.Parse("192.168.1.100"));
+ }
+
+ [Fact]
+ public void GetAddressCmdlet_HostNameProperty_Exists()
+ {
+ var cmdlet = new GetAddressCmdlet();
+ cmdlet.HostName = "server01.example.com";
+ cmdlet.HostName.Should().Be("server01.example.com");
+ }
+
+ [Fact]
+ public void GetAddressCmdlet_HostBaseProperty_Exists()
+ {
+ var cmdlet = new GetAddressCmdlet();
+ cmdlet.HostBase = "server";
+ cmdlet.HostBase.Should().Be("server");
+ }
+
+ [Fact]
+ public void GetAddressCmdlet_TagIdProperty_Exists()
+ {
+ var cmdlet = new GetAddressCmdlet();
+ cmdlet.TagId = 2;
+ cmdlet.TagId.Should().Be(2);
+ }
+
+ [Fact]
+ public void GetAddressCmdlet_SubnetIdProperty_Exists()
+ {
+ var cmdlet = new GetAddressCmdlet();
+ cmdlet.SubnetId = 10;
+ cmdlet.SubnetId.Should().Be(10);
+ }
+
+ [Fact]
+ public void GetAddressCmdlet_SubnetCIDRProperty_Exists()
+ {
+ var cmdlet = new GetAddressCmdlet();
+ cmdlet.SubnetCIDR = "192.168.1.0/24";
+ cmdlet.SubnetCIDR.Should().Be("192.168.1.0/24");
+ }
+
+ // Test the underlying RequestHelper functionality that the cmdlet uses
+
+ [Fact]
+ public async Task RequestHelper_GetAddressById_ReturnsAddress()
+ {
+ // Arrange
+ SetupSession();
+ var handler = SetupMockHandler();
+ var addressJson = @"{
+ ""id"": 1,
+ ""subnetId"": 10,
+ ""ip"": ""192.168.1.100"",
+ ""hostname"": ""server01"",
+ ""description"": ""Test server""
+ }";
+ 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 RequestHelper_GetAddressById_BuildsCorrectUri()
+ {
+ // Arrange
+ SetupSession();
+ var handler = SetupMockHandler();
+ handler.WithSuccessResponse(@"{""id"": 42, ""ip"": ""10.0.0.1""}");
+
+ // Act
+ await RequestHelper.InvokeRequest(
+ "GET",
+ controllers.addresses,
+ types.Address,
+ null,
+ null,
+ new[] { "42" }
+ );
+
+ // Assert
+ handler.LastRequest!.RequestUri!.ToString().Should().Contain("/addresses/42/");
+ }
+
+ [Fact]
+ public async Task RequestHelper_SearchByIP_BuildsCorrectUri()
+ {
+ // Arrange
+ SetupSession();
+ var handler = SetupMockHandler();
+ handler.WithSuccessResponse(@"{""id"": 1, ""ip"": ""192.168.1.50""}");
+
+ // Act
+ await RequestHelper.InvokeRequest(
+ "GET",
+ controllers.addresses,
+ types.Address,
+ null,
+ null,
+ new[] { "search", "192.168.1.50" }
+ );
+
+ // Assert
+ handler.LastRequest!.RequestUri!.ToString().Should().Contain("/addresses/search/192.168.1.50/");
+ }
+
+ [Fact]
+ public async Task RequestHelper_SearchByHostname_BuildsCorrectUri()
+ {
+ // Arrange
+ SetupSession();
+ var handler = SetupMockHandler();
+ handler.WithSuccessResponse(@"{""id"": 1, ""ip"": ""10.0.0.5"", ""hostname"": ""myserver.example.com""}");
+
+ // Act
+ await RequestHelper.InvokeRequest(
+ "GET",
+ controllers.addresses,
+ types.Address,
+ null,
+ null,
+ new[] { "search_hostname", "myserver.example.com" }
+ );
+
+ // Assert
+ handler.LastRequest!.RequestUri!.ToString().Should().Contain("/addresses/search_hostname/myserver.example.com/");
+ }
+
+ [Fact]
+ public async Task RequestHelper_GetSubnetAddresses_BuildsCorrectUri()
+ {
+ // Arrange
+ SetupSession();
+ var handler = SetupMockHandler();
+ handler.WithSuccessResponse(@"[{""id"": 1, ""ip"": ""192.168.1.1""}]");
+
+ // Act
+ await RequestHelper.InvokeRequest(
+ "GET",
+ controllers.subnets,
+ types.Address,
+ null,
+ null,
+ new[] { "10", "addresses" }
+ );
+
+ // Assert
+ handler.LastRequest!.RequestUri!.ToString().Should().Contain("/subnets/10/addresses/");
+ }
+
+ [Fact]
+ public async Task RequestHelper_GetAddressesByTag_BuildsCorrectUri()
+ {
+ // Arrange
+ SetupSession();
+ var handler = SetupMockHandler();
+ handler.WithSuccessResponse(@"[{""id"": 1, ""ip"": ""10.0.0.1"", ""tag"": 2}]");
+
+ // Act
+ await RequestHelper.InvokeRequest(
+ "GET",
+ controllers.addresses,
+ types.Address,
+ null,
+ null,
+ new[] { "tags", "2", "addresses" }
+ );
+
+ // Assert
+ handler.LastRequest!.RequestUri!.ToString().Should().Contain("/addresses/tags/2/addresses/");
+ }
+
+ [Fact]
+ public async Task RequestHelper_ReturnsMultipleAddresses()
+ {
+ // Arrange
+ SetupSession();
+ var handler = SetupMockHandler();
+ handler.WithSuccessResponse(@"[
+ {""id"": 1, ""ip"": ""192.168.1.1""},
+ {""id"": 2, ""ip"": ""192.168.1.2""},
+ {""id"": 3, ""ip"": ""192.168.1.3""}
+ ]");
+
+ // Act
+ var result = await RequestHelper.InvokeRequest(
+ "GET",
+ controllers.subnets,
+ types.Address,
+ null,
+ null,
+ new[] { "10", "addresses" }
+ );
+
+ // Assert
+ result.Should().BeAssignableTo();
+ var addresses = ((System.Collections.IEnumerable)result!).Cast