diff --git a/.github/workflows/4_6_2_Core_Unit_Tests_Win.yaml b/.github/workflows/4_6_2_Core_Unit_Tests_Win.yaml new file mode 100644 index 000000000..56e5e6aed --- /dev/null +++ b/.github/workflows/4_6_2_Core_Unit_Tests_Win.yaml @@ -0,0 +1,114 @@ +name: build and test .NET 4.6.2 Windows + +on: + push: + pull_request: + branches: [ release, development ] + paths: + - '**.cs' + - '**.csproj' + +env: + DOTNET_VERSION: '8.0.100' # The .NET SDK version to use + +jobs: + build-and-test: + # if: ${{ ! always() }} + name: build-and-test-windows + runs-on: windows-latest + steps: + - name: Clone webprofusion/certify + uses: actions/checkout@master + with: + path: ./certify + + - name: Clone webprofusion/anvil + uses: actions/checkout@master + with: + repository: webprofusion/anvil + ref: refs/heads/main + path: ./libs/anvil + + - name: Clone webprofusion/certify-plugins (development branch push) + if: ${{ github.event_name == 'push' && contains(github.ref_name, '_dev') }} + uses: actions/checkout@master + with: + repository: webprofusion/certify-plugins + ref: refs/heads/development + path: ./certify-plugins + + - name: Clone webprofusion/certify-plugins (release branch push) + if: ${{ github.event_name == 'push' && contains(github.ref_name, '_rel') }} + uses: actions/checkout@master + with: + repository: webprofusion/certify-plugins + ref: refs/heads/release + path: ./certify-plugins + + - name: Clone webprofusion/certify-plugins (pull request) + if: ${{ github.event_name == 'pull_request' }} + uses: actions/checkout@master + with: + repository: webprofusion/certify-plugins + ref: ${{ github.base_ref }} + path: ./certify-plugins + + - name: Setup .NET Core + uses: actions/setup-dotnet@master + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Setup Step CLI + run: | + Invoke-WebRequest -Method 'GET' -uri 'https://dl.smallstep.com/gh-release/cli/docs-cli-install/v0.24.4/step_windows_0.24.4_amd64.zip' -Outfile 'C:\temp\step_windows_0.24.4_amd64.zip' + tar -oxzf C:\temp\step_windows_0.24.4_amd64.zip -C "C:\Program Files" + echo "C:\Program Files\step_0.24.4\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Pull step-ca Docker Image + run: docker pull jrnelson90/step-ca-win + + - name: Cache NuGet Dependencies + uses: actions/cache@v3 + with: + path: ~/.nuget/packages + # Look to see if there is a cache hit for the corresponding requirements file + key: ${{ runner.os }}-4.6.2-nuget-${{ hashFiles('./certify/src/Certify.Tests/Certify.Core.Tests.Unit/*.csproj') }} + restore-keys: | + ${{ runner.os }}-4.6.2-nuget + + - name: Install Dependencies & Build Certify.Core.Tests.Unit + run: | + dotnet tool install --global dotnet-reportgenerator-globaltool --version 5.2.0 + dotnet add package GitHubActionsTestLogger + dotnet add package coverlet.collector + dotnet build -c Debug -f net462 --property WarningLevel=0 /clp:ErrorsOnly + working-directory: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit + + - name: Run Certify.Core.Tests.Unit Tests + run: | + $env:GITHUB_WORKSPACE="$env:GITHUB_WORKSPACE\certify" + $env:GITHUB_STEP_SUMMARY=".\TestResults-4_6_2-${{ runner.os }}\test-summary.md" + dotnet test --no-build -f net462 -l "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true;annotations.messageFormat=@error\n@trace" + working-directory: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit + + - name: Generated Test Results Report + run: | + echo "# Test Results" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 + (Get-Content -Path .\TestResults-4_6_2-${{ runner.os }}\test-summary.md).Replace('
', '
') | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append + working-directory: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit + if: ${{ always() }} + + - name: Generated Test Coverage Report + run: | + reportgenerator -reports:./TestResults-4_6_2-${{ runner.os }}/*/*.cobertura.xml -targetdir:./TestResults-4_6_2-${{ runner.os }} -reporttypes:MarkdownSummaryGithub "-title:Test Coverage" + Get-Content -Path ./TestResults-4_6_2-${{ runner.os }}/SummaryGithub.md | Out-File -FilePath $env:GITHUB_STEP_SUMMARY + working-directory: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit + if: ${{ always() }} + + # - name: Upload dotnet test Artifacts + # uses: actions/upload-artifact@master + # with: + # name: dotnet-results-${{ runner.os }}-${{ env.DOTNET_VERSION }} + # path: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit/TestResults-4_6_2-${{ runner.os }} + # # Use always() to always run this step to publish test results when there are test failures + # if: ${{ always() }} diff --git a/.github/workflows/8_0_Core_Unit_Tests_Linux.yaml b/.github/workflows/8_0_Core_Unit_Tests_Linux.yaml new file mode 100644 index 000000000..6475b8460 --- /dev/null +++ b/.github/workflows/8_0_Core_Unit_Tests_Linux.yaml @@ -0,0 +1,114 @@ +name: build and test .NET Core 8.0 Linux + +on: + push: + pull_request: + branches: [ release, development ] + paths: + - '**.cs' + - '**.csproj' + +env: + DOTNET_VERSION: '8.0.100' # The .NET SDK version to use + +jobs: + build-and-test: + + name: build-and-test-linux + runs-on: ubuntu-latest + steps: + - name: Clone webprofusion/certify + uses: actions/checkout@master + with: + path: ./certify + + - name: Clone webprofusion/anvil + uses: actions/checkout@master + with: + repository: webprofusion/anvil + ref: refs/heads/main + path: ./libs/anvil + + - name: Clone webprofusion/certify-plugins (development branch push) + if: ${{ github.event_name == 'push' && contains(github.ref_name, '_dev') }} + uses: actions/checkout@master + with: + repository: webprofusion/certify-plugins + ref: refs/heads/development + path: ./certify-plugins + + - name: Clone webprofusion/certify-plugins (release branch push) + if: ${{ github.event_name == 'push' && contains(github.ref_name, '_rel') }} + uses: actions/checkout@master + with: + repository: webprofusion/certify-plugins + ref: refs/heads/release + path: ./certify-plugins + + - name: Clone webprofusion/certify-plugins (pull request) + if: ${{ github.event_name == 'pull_request' }} + uses: actions/checkout@master + with: + repository: webprofusion/certify-plugins + ref: ${{ github.base_ref }} + path: ./certify-plugins + + - name: Setup .NET Core + uses: actions/setup-dotnet@master + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Setup Step CLI + run: | + wget https://dl.smallstep.com/gh-release/cli/docs-cli-install/v0.23.0/step-cli_0.23.0_amd64.deb + sudo dpkg -i step-cli_0.23.0_amd64.deb + + - name: Pull step-ca Docker Image + run: docker pull smallstep/step-ca + + - name: Cache NuGet Dependencies + uses: actions/cache@v3 + with: + path: ~/.nuget/packages + # Look to see if there is a cache hit for the corresponding requirements file + key: ${{ runner.os }}-${{ env.DOTNET_VERSION }}-nuget-${{ hashFiles('./certify/src/Certify.Tests/Certify.Core.Tests.Unit/*.csproj') }} + restore-keys: | + ${{ runner.os }}-${{ env.DOTNET_VERSION }}-nuget + + - name: Install Dependencies & Build Certify.Core.Tests.Unit + run: | + dotnet tool install --global dotnet-reportgenerator-globaltool --version 5.2.0 + dotnet add package GitHubActionsTestLogger + dotnet add package coverlet.collector + dotnet build -c Debug -f net8.0 --property WarningLevel=0 /clp:ErrorsOnly + working-directory: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit + + - name: Run Certify.Core.Tests.Unit Tests + run: | + export GITHUB_WORKSPACE="$GITHUB_WORKSPACE/certify" + export GITHUB_STEP_SUMMARY="./TestResults-8_0-${{ runner.os }}/test-summary.md" + dotnet test --no-build -f net8.0 -l "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true;annotations.messageFormat=@error\n@trace" + working-directory: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit + + - name: Generate Test Results Report + run: | + echo "# Test Results" > $GITHUB_STEP_SUMMARY + sed -i 's/
/
/g' ./TestResults-8_0-${{ runner.os }}/test-summary.md + cat ./TestResults-8_0-${{ runner.os }}/test-summary.md >> $GITHUB_STEP_SUMMARY + working-directory: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit + if: ${{ always() }} + + - name: Generated Test Coverage Report + run: | + reportgenerator -reports:./TestResults-8_0-${{ runner.os }}/*/coverage.cobertura.xml -targetdir:./TestResults-8_0-${{ runner.os }} -reporttypes:MarkdownSummaryGithub "-title:Test Coverage" + cat ./TestResults-8_0-${{ runner.os }}/SummaryGithub.md > $GITHUB_STEP_SUMMARY + working-directory: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit + if: ${{ always() }} + + # - name: Upload dotnet test Artifacts + # uses: actions/upload-artifact@master + # with: + # name: dotnet-results-${{ runner.os }}-${{ env.DOTNET_VERSION }} + # path: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit/TestResults-8_0-${{ runner.os }} + # # Use always() to always run this step to publish test results when there are test failures + # if: ${{ always() }} diff --git a/.github/workflows/8_0_Core_Unit_Tests_Win.yaml b/.github/workflows/8_0_Core_Unit_Tests_Win.yaml new file mode 100644 index 000000000..233d3f5e2 --- /dev/null +++ b/.github/workflows/8_0_Core_Unit_Tests_Win.yaml @@ -0,0 +1,114 @@ +name: build and test .NET Core 8.0 Windows + +on: + push: + pull_request: + branches: [ release, development ] + paths: + - '**.cs' + - '**.csproj' + +env: + DOTNET_VERSION: '8.0.100' # The .NET SDK version to use + +jobs: + build-and-test: + + name: build-and-test-windows + runs-on: windows-latest + steps: + - name: Clone webprofusion/certify + uses: actions/checkout@master + with: + path: ./certify + + - name: Clone webprofusion/anvil + uses: actions/checkout@master + with: + repository: webprofusion/anvil + ref: refs/heads/main + path: ./libs/anvil + + - name: Clone webprofusion/certify-plugins (development branch push) + if: ${{ github.event_name == 'push' && contains(github.ref_name, '_dev') }} + uses: actions/checkout@master + with: + repository: webprofusion/certify-plugins + ref: refs/heads/development + path: ./certify-plugins + + - name: Clone webprofusion/certify-plugins (release branch push) + if: ${{ github.event_name == 'push' && contains(github.ref_name, '_rel') }} + uses: actions/checkout@master + with: + repository: webprofusion/certify-plugins + ref: refs/heads/release + path: ./certify-plugins + + - name: Clone webprofusion/certify-plugins (pull request) + if: ${{ github.event_name == 'pull_request' }} + uses: actions/checkout@master + with: + repository: webprofusion/certify-plugins + ref: ${{ github.base_ref }} + path: ./certify-plugins + + - name: Setup .NET Core + uses: actions/setup-dotnet@master + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Setup Step CLI + run: | + Invoke-WebRequest -Method 'GET' -uri 'https://dl.smallstep.com/gh-release/cli/docs-cli-install/v0.24.4/step_windows_0.24.4_amd64.zip' -Outfile 'C:\temp\step_windows_0.24.4_amd64.zip' + tar -oxzf C:\temp\step_windows_0.24.4_amd64.zip -C "C:\Program Files" + echo "C:\Program Files\step_0.24.4\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Pull step-ca Docker Image + run: docker pull jrnelson90/step-ca-win + + - name: Cache NuGet Dependencies + uses: actions/cache@v3 + with: + path: ~/.nuget/packages + # Look to see if there is a cache hit for the corresponding requirements file + key: ${{ runner.os }}-${{ env.DOTNET_VERSION }}-nuget-${{ hashFiles('./certify/src/Certify.Tests/Certify.Core.Tests.Unit/*.csproj') }} + restore-keys: | + ${{ runner.os }}-${{ env.DOTNET_VERSION }}-nuget + + - name: Install Dependencies & Build Certify.Core.Tests.Unit + run: | + dotnet tool install --global dotnet-reportgenerator-globaltool --version 5.2.0 + dotnet add package GitHubActionsTestLogger + dotnet add package coverlet.collector + dotnet build -c Debug -f net8.0 --property WarningLevel=0 /clp:ErrorsOnly + working-directory: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit + + - name: Run Certify.Core.Tests.Unit Tests + run: | + $env:GITHUB_WORKSPACE="$env:GITHUB_WORKSPACE\certify" + $env:GITHUB_STEP_SUMMARY=".\TestResults-8_0-${{ runner.os }}\test-summary.md" + dotnet test --no-build -f net8.0 -l "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true;annotations.messageFormat=@error\n@trace" + working-directory: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit + + - name: Generate Test Results Report + run: | + echo "# Test Results" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 + (Get-Content -Path .\TestResults-8_0-${{ runner.os }}\test-summary.md).Replace('
', '
') | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append + working-directory: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit + if: ${{ always() }} + + - name: Generated Test Coverage Report + run: | + reportgenerator -reports:./TestResults-8_0-${{ runner.os }}/*/coverage.cobertura.xml -targetdir:./TestResults-8_0-${{ runner.os }} -reporttypes:MarkdownSummaryGithub "-title:Test Coverage" + Get-Content -Path ./TestResults-8_0-${{ runner.os }}/SummaryGithub.md | Out-File -FilePath $env:GITHUB_STEP_SUMMARY + working-directory: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit + if: ${{ always() }} + + # - name: Upload dotnet test Artifacts + # uses: actions/upload-artifact@master + # with: + # name: dotnet-results-${{ runner.os }}-${{ env.DOTNET_VERSION }} + # path: ./certify/src/Certify.Tests/Certify.Core.Tests.Unit/TestResults-8_0-${{ runner.os }} + # # Use always() to always run this step to publish test results when there are test failures + # if: ${{ always() }} diff --git a/src/Certify.Core/Management/CertifyManager/CertifyManager.cs b/src/Certify.Core/Management/CertifyManager/CertifyManager.cs index 8703a2596..9407ceafb 100644 --- a/src/Certify.Core/Management/CertifyManager/CertifyManager.cs +++ b/src/Certify.Core/Management/CertifyManager/CertifyManager.cs @@ -537,7 +537,16 @@ public async Task PerformRenewalTasks() return await Task.FromResult(true); } - public void Dispose() => ManagedCertificateLog.DisposeLoggers(); + public void Dispose() => Cleanup(); + + private void Cleanup() + { + ManagedCertificateLog.DisposeLoggers(); + if(_tc != null) + { + _tc.Dispose(); + } + } /// /// Perform (or preview) an import of settings from another instance diff --git a/src/Certify.Tests/Certify.Core.Tests.Integration/CertRequestTests.cs b/src/Certify.Tests/Certify.Core.Tests.Integration/CertRequestTests.cs index 501c071eb..0e89e1cf3 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Integration/CertRequestTests.cs +++ b/src/Certify.Tests/Certify.Core.Tests.Integration/CertRequestTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -81,6 +81,7 @@ public async Task TeardownIIS() { await iisManager.DeleteSite(testSiteName); Assert.IsFalse(await iisManager.SiteExists(testSiteName)); + certifyManager.Dispose(); } [TestMethod, TestCategory("MegaTest")] diff --git a/src/Certify.Tests/Certify.Core.Tests.Integration/CertifyManagerServerTypeTests.cs b/src/Certify.Tests/Certify.Core.Tests.Integration/CertifyManagerServerTypeTests.cs index cfa981f6b..8fcc9840e 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Integration/CertifyManagerServerTypeTests.cs +++ b/src/Certify.Tests/Certify.Core.Tests.Integration/CertifyManagerServerTypeTests.cs @@ -46,6 +46,7 @@ public async Task TeardownIIS() { await _iisManager.DeleteSite(_testSiteName); Assert.IsFalse(await _iisManager.SiteExists(_testSiteName)); + _certifyManager.Dispose(); } [TestMethod, Description("Happy path test for using CertifyManager.GetPrimaryWebSites() for IIS")] diff --git a/src/Certify.Tests/Certify.Core.Tests.Integration/CertifyManagerTests.cs b/src/Certify.Tests/Certify.Core.Tests.Integration/CertifyManagerTests.cs index 9f3f4c649..d5052a534 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Integration/CertifyManagerTests.cs +++ b/src/Certify.Tests/Certify.Core.Tests.Integration/CertifyManagerTests.cs @@ -19,6 +19,11 @@ public CertifyManagerTests() _certifyManager.Init().Wait(); } + [TestCleanup] public void Cleanup() + { + _certifyManager.Dispose(); + } + [TestMethod, Description("Happy path test for using CertifyManager.GetACMEProvider()")] public async Task TestCertifyManagerGetACMEProvider() { diff --git a/src/Certify.Tests/Certify.Core.Tests.Integration/DeploymentPreviewTests.cs b/src/Certify.Tests/Certify.Core.Tests.Integration/DeploymentPreviewTests.cs index 038c76688..2970281ff 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Integration/DeploymentPreviewTests.cs +++ b/src/Certify.Tests/Certify.Core.Tests.Integration/DeploymentPreviewTests.cs @@ -74,6 +74,7 @@ public async Task TeardownIIS() { await iisManager.DeleteSite(testSiteName); Assert.IsFalse(await iisManager.SiteExists(testSiteName)); + certifyManager.Dispose(); } [TestMethod] diff --git a/src/Certify.Tests/Certify.Core.Tests.Integration/DeploymentTaskTests.cs b/src/Certify.Tests/Certify.Core.Tests.Integration/DeploymentTaskTests.cs index 3af000cd7..3022f1216 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Integration/DeploymentTaskTests.cs +++ b/src/Certify.Tests/Certify.Core.Tests.Integration/DeploymentTaskTests.cs @@ -36,6 +36,12 @@ public DeploymentTaskTests() PrimaryTestDomain = ConfigSettings["Cloudflare_TestDomain"]; } + [TestCleanup] + public void Cleanup() + { + certifyManager?.Dispose(); + } + private DeploymentTaskConfig GetMockTaskConfig( string name, string msg = "Hello World", diff --git a/src/Certify.Tests/Certify.Core.Tests.Integration/RdapTests.cs b/src/Certify.Tests/Certify.Core.Tests.Integration/RdapTests.cs index 743806cf8..62292dba2 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Integration/RdapTests.cs +++ b/src/Certify.Tests/Certify.Core.Tests.Integration/RdapTests.cs @@ -8,12 +8,6 @@ namespace Certify.Core.Tests [TestClass] public class RdapTests { - - public RdapTests() - { - - } - [TestMethod, Description("Test Rdap Query")] [DataTestMethod] [DataRow("example.com", "OK", null)] diff --git a/src/Certify.Tests/Certify.Core.Tests.Unit/CAFailoverTests.cs b/src/Certify.Tests/Certify.Core.Tests.Unit/CAFailoverTests.cs index 0555e39aa..b47eb7afe 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Unit/CAFailoverTests.cs +++ b/src/Certify.Tests/Certify.Core.Tests.Unit/CAFailoverTests.cs @@ -203,7 +203,7 @@ public void TestBasicNoFallbacks() var selectedAccount = RenewalManager.SelectCAWithFailover(caList, accounts.FindAll(a => a.IsStagingAccount == false), managedCertificate, defaultCAAccount); // assert result - Assert.IsTrue(selectedAccount.CertificateAuthorityId == DEFAULTCA, "Default CA should be selected"); + Assert.IsTrue(selectedAccount.CertificateAuthorityId == DEFAULTCA, $"Default CA should be selected: returned CA {selectedAccount.CertificateAuthorityId}"); Assert.IsFalse(selectedAccount.IsFailoverSelection, "Account should not be marked as a failover choice"); } @@ -231,7 +231,7 @@ public void TestBasicNextFallbackNull() var selectedAccount = RenewalManager.SelectCAWithFailover(caList, accounts, managedCertificate, defaultCAAccount); // assert result - Assert.IsTrue(selectedAccount.CertificateAuthorityId == DEFAULTCA, "Default CA should be selected"); + Assert.IsTrue(selectedAccount.CertificateAuthorityId == DEFAULTCA, $"Default CA should be selected: returned CA {selectedAccount.CertificateAuthorityId}"); Assert.IsFalse(selectedAccount.IsFailoverSelection, "Account should not be marked as a failover choice"); } diff --git a/src/Certify.Tests/Certify.Core.Tests.Unit/Certify.Core.Tests.Unit.csproj b/src/Certify.Tests/Certify.Core.Tests.Unit/Certify.Core.Tests.Unit.csproj index 1273b3f7d..be055e774 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Unit/Certify.Core.Tests.Unit.csproj +++ b/src/Certify.Tests/Certify.Core.Tests.Unit/Certify.Core.Tests.Unit.csproj @@ -101,6 +101,15 @@ 1701;1702;NU1701 + + .\unit-test-462.runsettings + + + .\unit-test-8-0.runsettings + + + .\unit-test-8-0-linux.runsettings + @@ -132,6 +141,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Certify.Tests/Certify.Core.Tests.Unit/CertifyManagerAccountTests.cs b/src/Certify.Tests/Certify.Core.Tests.Unit/CertifyManagerAccountTests.cs index 1aa31f23d..44ca2ef33 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Unit/CertifyManagerAccountTests.cs +++ b/src/Certify.Tests/Certify.Core.Tests.Unit/CertifyManagerAccountTests.cs @@ -6,6 +6,7 @@ using System.Net.Http; using System.Runtime.InteropServices; using System.Text; +using System.Threading; using System.Threading.Tasks; using Certify.ACME.Anvil; using Certify.Management; @@ -21,35 +22,90 @@ namespace Certify.Core.Tests.Unit { [TestClass] - public class CertifyManagerAccountTests : IDisposable + public class CertifyManagerAccountTests { - private readonly CertifyManager _certifyManager; - private readonly bool _isContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true"; - private readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - private readonly string _caDomain; - private readonly int _caPort; - private IContainer _caContainer; - private IVolume _stepVolume; + private static readonly bool _isContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true"; + private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + private static readonly string _winRunnerTempDir = "C:\\Temp\\.step"; + private static string _caDomain; + private static int _caPort; + private static IContainer _caContainer; + private static IVolume _stepVolume; + private static Loggy _log; + private CertifyManager _certifyManager; private CertificateAuthority _customCa; private AccountDetails _customCaAccount; - private readonly Loggy _log; - public CertifyManagerAccountTests() + [ClassInitialize] + public static async Task ClassInit(TestContext context) { - _certifyManager = new CertifyManager(); - _certifyManager.Init().Wait(); _log = new Loggy(new LoggerConfiguration().WriteTo.Debug().CreateLogger()); _caDomain = _isContainer ? "step-ca" : "localhost"; _caPort = 9000; - BootstrapStepCa().Wait(); - CheckCustomCaIsRunning().Wait(); - AddCustomCa().Wait(); - AddNewAccount().Wait(); + await BootstrapStepCa(); + await CheckCustomCaIsRunning(); } - private async Task BootstrapStepCa() + [TestInitialize] + public async Task TestInit() + { + _certifyManager = new CertifyManager(); + _certifyManager.Init().Wait(); + + await AddCustomCa(); + await AddNewCustomCaAccount(); + await CheckForExistingLeAccount(); + } + + [TestCleanup] + public async Task Cleanup() + { + if (_customCaAccount != null) + { + await _certifyManager.RemoveAccount(_customCaAccount.StorageKey, true); + } + + if (_customCa != null) + { + await _certifyManager.RemoveCertificateAuthority(_customCa.Id); + } + + _certifyManager?.Dispose(); + } + + [ClassCleanup(ClassCleanupBehavior.EndOfClass)] + public static async Task ClassCleanup() + { + if (!_isContainer) + { + await _caContainer.DisposeAsync(); + if (_stepVolume != null) + { + await _stepVolume.DeleteAsync(); + await _stepVolume.DisposeAsync(); + } + else + { + Directory.Delete(_winRunnerTempDir, true); + } + } + + var stepConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".step", "config"); + if (Directory.Exists(stepConfigPath)) + { + Directory.Delete(stepConfigPath, true); + } + + var stepCertsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".step", "certs"); + if (Directory.Exists(stepCertsPath)) + { + Directory.Delete(stepCertsPath, true); + } + } + + private static async Task BootstrapStepCa() { string stepCaFingerprint; @@ -68,45 +124,81 @@ private async Task BootstrapStepCa() } else { + var dockerInfo = RunCommand("docker", "info --format \"{{ .OSType }}\"", "Get Docker Info"); + var runningWindowsDockerEngine = dockerInfo.output.Contains("windows"); + // Start new step-ca container - await StartStepCaContainer(); + await StartStepCaContainer(runningWindowsDockerEngine); // Read step-ca fingerprint from config file - var stepCaConfigBytes = await _caContainer.ReadFileAsync("/home/step/config/defaults.json"); - var stepCaConfigJson = JsonReader.ReadBytes(stepCaConfigBytes); - stepCaFingerprint = stepCaConfigJson.fingerprint; + if (_isWindows && runningWindowsDockerEngine) + { + // Read step-ca fingerprint from config file + var stepCaConfigJson = JsonReader.ReadFile($"{_winRunnerTempDir}\\config\\defaults.json"); + stepCaFingerprint = stepCaConfigJson.fingerprint; + } else + { + var stepCaConfigBytes = await _caContainer.ReadFileAsync("/home/step/config/defaults.json"); + var stepCaConfigJson = JsonReader.ReadBytes(stepCaConfigBytes); + stepCaFingerprint = stepCaConfigJson.fingerprint; + } } // Run bootstrap command - //var command = $"ca bootstrap -f --ca-url https://{_caDomain}:{_caPort} --fingerprint {stepCaFingerprint} --install"; - var command = $"ca bootstrap -f --ca-url https://{_caDomain}:{_caPort} --fingerprint {stepCaFingerprint}"; - RunCommand("step", command, "Bootstrap Step CA Script", 1000 * 30); + var args = $"ca bootstrap -f --ca-url https://{_caDomain}:{_caPort} --fingerprint {stepCaFingerprint}"; + RunCommand("step", args, "Bootstrap Step CA Script", 1000 * 30); } - private async Task StartStepCaContainer() + private static async Task StartStepCaContainer(bool runningWindowsDockerEngine) { try { - // Create new volume for step-ca container - _stepVolume = new VolumeBuilder().WithName("step").Build(); - await _stepVolume.CreateAsync(); - - // Create new step-ca container - _caContainer = new ContainerBuilder() - .WithName("step-ca") - // Set the image for the container to "smallstep/step-ca:latest". - .WithImage("smallstep/step-ca:latest") - .WithVolumeMount(_stepVolume, "/home/step") - // Bind port 9000 of the container to port 9000 on the host. - .WithPortBinding(_caPort) - .WithEnvironment("DOCKER_STEPCA_INIT_NAME", "Smallstep") - .WithEnvironment("DOCKER_STEPCA_INIT_DNS_NAMES", _caDomain) - .WithEnvironment("DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT", "true") - .WithEnvironment("DOCKER_STEPCA_INIT_ACME", "true") - // Wait until the HTTPS endpoint of the container is available. - .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged($"Serving HTTPS on :{_caPort} ...")) - // Build the container configuration. - .Build(); + if (_isWindows && runningWindowsDockerEngine) + { + if (!Directory.Exists(_winRunnerTempDir)) { + Directory.CreateDirectory(_winRunnerTempDir); + } + + // Create new step-ca container + _caContainer = new ContainerBuilder() + .WithName("step-ca") + // Set the image for the container to "jrnelson90/step-ca-win:latest". + .WithImage("jrnelson90/step-ca-win:latest") + .WithBindMount(_winRunnerTempDir, "C:\\Users\\ContainerUser\\.step") + // Bind port 9000 of the container to port 9000 on the host. + .WithPortBinding(_caPort) + .WithEnvironment("DOCKER_STEPCA_INIT_NAME", "Smallstep") + .WithEnvironment("DOCKER_STEPCA_INIT_DNS_NAMES", _caDomain) + .WithEnvironment("DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT", "true") + .WithEnvironment("DOCKER_STEPCA_INIT_ACME", "true") + // Wait until the HTTPS endpoint of the container is available. + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged($"Serving HTTPS on :{_caPort} ...")) + // Build the container configuration. + .Build(); + } + else + { + // Create new volume for step-ca container + _stepVolume = new VolumeBuilder().WithName("step").Build(); + await _stepVolume.CreateAsync(); + + // Create new step-ca container + _caContainer = new ContainerBuilder() + .WithName("step-ca") + // Set the image for the container to "smallstep/step-ca:latest". + .WithImage("smallstep/step-ca:latest") + .WithVolumeMount(_stepVolume, "/home/step") + // Bind port 9000 of the container to port 9000 on the host. + .WithPortBinding(_caPort) + .WithEnvironment("DOCKER_STEPCA_INIT_NAME", "Smallstep") + .WithEnvironment("DOCKER_STEPCA_INIT_DNS_NAMES", _caDomain) + .WithEnvironment("DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT", "true") + .WithEnvironment("DOCKER_STEPCA_INIT_ACME", "true") + // Wait until the HTTPS endpoint of the container is available. + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged($"Serving HTTPS on :{_caPort} ...")) + // Build the container configuration. + .Build(); + } // Start step-ca container await _caContainer.StartAsync(); @@ -154,7 +246,7 @@ private class StepCaConfig public string root; } - private void RunCommand(string program, string args, string description = null, int timeoutMS = 1000 * 5) + private static CommandOutput RunCommand(string program, string args, string description = null, int timeoutMS = Timeout.Infinite) { if (description == null) { description = string.Concat(program, " ", args); } @@ -165,7 +257,6 @@ private void RunCommand(string program, string args, string description = null, { FileName = program, Arguments = args, - RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, @@ -199,7 +290,7 @@ private void RunCommand(string program, string args, string description = null, process.BeginOutputReadLine(); process.BeginErrorReadLine(); - process.WaitForExit(timeoutMS); + process.WaitForExit(timeoutMS); } catch (Exception exp) { @@ -207,10 +298,19 @@ private void RunCommand(string program, string args, string description = null, throw; } - _log.Information($"{description} Successful"); + _log.Information($"{description} is Finished"); + + return new CommandOutput { errorOutput = errorOutput, output = output, exitCode = process.ExitCode }; + } + + private struct CommandOutput + { + public string errorOutput { get; set; } + public string output { get; set; } + public int exitCode { get; set; } } - private async Task CheckCustomCaIsRunning() + private static async Task CheckCustomCaIsRunning() { var httpHandler = new HttpClientHandler(); @@ -244,7 +344,8 @@ private async Task AddCustomCa() CertAuthoritySupportedRequests.DOMAIN_MULTIPLE_SAN.ToString(), CertAuthoritySupportedRequests.DOMAIN_WILDCARD.ToString() }, - SupportedKeyTypes = new List{ + SupportedKeyTypes = new List + { StandardKeyTypes.ECDSA256, } }; @@ -252,7 +353,7 @@ private async Task AddCustomCa() Assert.IsTrue(updateCaRes.IsSuccess, $"Expected Custom CA creation for CA with ID {_customCa.Id} to be successful"); } - private async Task AddNewAccount() + private async Task AddNewCustomCaAccount() { if (_customCa?.Id != null) { @@ -273,40 +374,24 @@ private async Task AddNewAccount() } } - public void Dispose() => Cleanup().Wait(); - - private async Task Cleanup() + private async Task CheckForExistingLeAccount() { - if (_customCaAccount != null) - { - await _certifyManager.RemoveAccount(_customCaAccount.StorageKey, true); - } - - if (_customCa != null) - { - await _certifyManager.RemoveCertificateAuthority(_customCa.Id); - } - - if (!_isContainer) - { - await _caContainer.DisposeAsync(); - await _stepVolume.DeleteAsync(); - await _stepVolume.DisposeAsync(); - } - - var stepConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".step", "config"); - if (Directory.Exists(stepConfigPath)) + if ((await _certifyManager.GetAccountRegistrations()).Find(a => a.CertificateAuthorityId == "letsencrypt.org") == null) { - Directory.Delete(stepConfigPath, true); - } + var contactRegistration = new ContactRegistration + { + AgreedToTermsAndConditions = true, + CertificateAuthorityId = "letsencrypt.org", + EmailAddress = "admin." + Guid.NewGuid().ToString().Substring(0, 6) + "@test.com", + ImportedAccountKey = "", + ImportedAccountURI = "", + IsStaging = true + }; - var stepCertsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".step", "certs"); - if (Directory.Exists(stepCertsPath)) - { - Directory.Delete(stepCertsPath, true); + // Add account + var addAccountRes = await _certifyManager.AddAccount(contactRegistration); + Assert.IsTrue(addAccountRes.IsSuccess, $"Expected account creation to be successful for {contactRegistration.EmailAddress}"); } - - _certifyManager?.Dispose(); } [TestMethod, Description("Happy path test for using CertifyManager.GetAccountDetails()")] diff --git a/src/Certify.Tests/Certify.Core.Tests.Unit/CertifyServiceTests.cs b/src/Certify.Tests/Certify.Core.Tests.Unit/CertifyServiceTests.cs new file mode 100644 index 000000000..92a342980 --- /dev/null +++ b/src/Certify.Tests/Certify.Core.Tests.Unit/CertifyServiceTests.cs @@ -0,0 +1,430 @@ +using Certify.Models; +using Certify.Models.Config; +using Certify.Shared; +using Medallion.Shell; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Certify.Core.Tests.Unit +{ +#if NET462 + [TestClass] + public class CertifyServiceTests + { + private HttpClient _httpClient; + private string serviceUri; + + public CertifyServiceTests() { + var serviceConfig = SharedUtils.ServiceConfigManager.GetAppServiceConfig(); + serviceUri = $"{(serviceConfig.UseHTTPS ? "https" : "http")}://{serviceConfig.Host}:{serviceConfig.Port}"; + var httpHandler = new HttpClientHandler { UseDefaultCredentials = true }; + _httpClient = new HttpClient(httpHandler); + _httpClient.DefaultRequestHeaders.Add("User-Agent", "Certify/App"); + _httpClient.BaseAddress = new Uri(serviceUri+"/api/"); + } + + private async Task StartCertifyService(string args = "") + { + Command certifyService; + if (args == "") + { + certifyService = Command.Run(".\\Certify.Service.exe"); + await Task.Delay(2000); + } + else + { + certifyService = Command.Run(".\\Certify.Service.exe", args); + } + + return certifyService; + } + + private async Task StopCertifyService(Command certifyService) + { + await certifyService.TrySignalAsync(CommandSignal.ControlC); + + var cmdResult = await certifyService.Task; + + Assert.AreEqual(cmdResult.ExitCode, 0, "Unexpected exit code"); + + return cmdResult; + } + + [TestMethod, Description("Validate that Certify.Service.exe does not start with args from CLI")] + public async Task TestProgramMainFailsWithArgsCli() + { + var certifyService = await StartCertifyService("args"); + + var cmdResult = await certifyService.Task; + + Assert.IsTrue(cmdResult.StandardOutput.Contains("Topshelf.HostFactory Error: 0 : An exception occurred creating the host, Topshelf.HostConfigurationException: The service was not properly configured:")); + Assert.IsTrue(cmdResult.StandardOutput.Contains("Topshelf.HostFactory Error: 0 : The service terminated abnormally, Topshelf.HostConfigurationException: The service was not properly configured:")); + + Assert.AreEqual(cmdResult.ExitCode, 1067, "Unexpected exit code"); + } + + [TestMethod, Description("Validate that Certify.Service.exe starts from CLI with no args")] + public async Task TestProgramMainStartsCli() + { + var certifyService = await StartCertifyService(); + + var cmdResult = await StopCertifyService(certifyService); + + Assert.IsTrue(cmdResult.StandardOutput.Contains("[Success] Name Certify.Service")); + Assert.IsTrue(cmdResult.StandardOutput.Contains("[Success] DisplayName Certify Certificate Manager Service (Instance: Debug)")); + Assert.IsTrue(cmdResult.StandardOutput.Contains("[Success] Description Certify Certificate Manager Service")); + Assert.IsTrue(cmdResult.StandardOutput.Contains("[Success] InstanceName Debug")); + Assert.IsTrue(cmdResult.StandardOutput.Contains("[Success] ServiceName Certify.Service$Debug")); + Assert.IsTrue(cmdResult.StandardOutput.Contains("The Certify.Service$Debug service is now running, press Control+C to exit.")); + Assert.IsTrue(cmdResult.StandardOutput.Contains("Control+C detected, attempting to stop service.")); + Assert.IsTrue(cmdResult.StandardOutput.Contains("The Certify.Service$Debug service has stopped.")); + } + + [TestMethod, Description("Validate that Certify.Service.exe returns a valid response on route GET /api/system/appversion")] + public async Task TestCertifyServiceAppVersionRoute() + { + var certifyService = await StartCertifyService(); + + try + { + var versionRawRes = await _httpClient.GetAsync("system/appversion"); + var versionResStr = await versionRawRes.Content.ReadAsStringAsync(); + var versionRes = JsonConvert.DeserializeObject(versionResStr); + + Assert.AreEqual(HttpStatusCode.OK, versionRawRes.StatusCode, $"Unexpected status code from GET {versionRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + StringAssert.Matches(versionRes, new Regex(@"^(\d+\.)?(\d+\.)?(\d+\.)?(\*|\d+)$"), $"Unexpected response from GET {versionRawRes.RequestMessage.RequestUri.AbsoluteUri} : {versionResStr}"); + } + finally + { + await StopCertifyService(certifyService); + } + } + + [TestMethod, Description("Validate that Certify.Service.exe returns a valid respose on route GET /api/system/updatecheck")] + public async Task TestCertifyServiceUpdateCheckRoute() + { + var certifyService = await StartCertifyService(); + + try + { + var updatesRawRes = await _httpClient.GetAsync("system/updatecheck"); + var updateRawResStr = await updatesRawRes.Content.ReadAsStringAsync(); + var updateRes = JsonConvert.DeserializeObject(updateRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, updatesRawRes.StatusCode, $"Unexpected status code from GET {updatesRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsFalse(updateRes.MustUpdate); + Assert.IsFalse(updateRes.IsNewerVersion); + Assert.AreEqual(updateRes.InstalledVersion.ToString(), updateRes.Version.ToString()); + Assert.AreEqual("", updateRes.UpdateFilePath); + } + finally + { + await StopCertifyService(certifyService); + } + } + + [TestMethod, Description("Validate that Certify.Service.exe returns a valid response on route GET /api/system/diagnostics")] + public async Task TestCertifyServiceDiagnosticsRoute() + { + var certifyService = await StartCertifyService(); + + try + { + var diagnosticsRawRes = await _httpClient.GetAsync("system/diagnostics"); + var diagnosticsRawResStr = await diagnosticsRawRes.Content.ReadAsStringAsync(); + var diagnosticsRes = JsonConvert.DeserializeObject>(diagnosticsRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, diagnosticsRawRes.StatusCode, $"Unexpected status code from GET {diagnosticsRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.AreEqual(4, diagnosticsRes.Count); + + Assert.AreEqual("Created test temp file OK.", diagnosticsRes[0].Message); + Assert.IsTrue(diagnosticsRes[0].IsSuccess); + Assert.IsFalse(diagnosticsRes[0].IsWarning); + Assert.AreEqual(null, diagnosticsRes[0].Result); + + Assert.AreEqual($"Drive {Environment.GetEnvironmentVariable("SystemDrive")} has more than 512MB of disk space free.", diagnosticsRes[1].Message); + Assert.IsTrue(diagnosticsRes[1].IsSuccess); + Assert.IsFalse(diagnosticsRes[1].IsWarning); + Assert.AreEqual(null, diagnosticsRes[1].Result); + + Assert.AreEqual("System time is correct.", diagnosticsRes[2].Message); + Assert.IsTrue(diagnosticsRes[2].IsSuccess); + Assert.IsFalse(diagnosticsRes[2].IsWarning); + Assert.AreEqual(null, diagnosticsRes[2].Result); + + Assert.AreEqual("PowerShell 5.0 or higher is available.", diagnosticsRes[3].Message); + Assert.IsTrue(diagnosticsRes[3].IsSuccess); + Assert.IsFalse(diagnosticsRes[3].IsWarning); + Assert.AreEqual(null, diagnosticsRes[3].Result); + } + finally + { + await StopCertifyService(certifyService); + } + } + + [TestMethod, Description("Validate that Certify.Service.exe returns a valid response on route GET /api/system/datastores/providers")] + public async Task TestCertifyServiceDatastoreProvidersRoute() + { + var certifyService = await StartCertifyService(); + + try + { + var datastoreProvidersRawRes = await _httpClient.GetAsync("system/datastores/providers"); + var datastoreProvidersRawResStr = await datastoreProvidersRawRes.Content.ReadAsStringAsync(); + var datastoreProvidersRes = JsonConvert.DeserializeObject>(datastoreProvidersRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreProvidersRawRes.StatusCode, $"Unexpected status code from GET {datastoreProvidersRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + } + finally + { + await StopCertifyService(certifyService); + } + } + + [TestMethod, Description("Validate that Certify.Service.exe returns a valid response on route GET /api/system/datastores/")] + public async Task TestCertifyServiceDatastoresRoute() + { + var certifyService = await StartCertifyService(); + + try + { + var datastoreRawRes = await _httpClient.GetAsync("system/datastores/"); + var datastoreRawResStr = await datastoreRawRes.Content.ReadAsStringAsync(); + var datastoreRes = JsonConvert.DeserializeObject>(datastoreRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreRawRes.StatusCode, $"Unexpected status code from GET {datastoreRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreRes.Count >= 1); + } + finally + { + await StopCertifyService(certifyService); + } + } + + [TestMethod, Description("Validate that Certify.Service.exe returns a valid response on route POST /api/system/datastores/test")] + public async Task TestCertifyServiceDatastoresTestRoute() + { + var certifyService = await StartCertifyService(); + + try + { + var datastoreRawRes = await _httpClient.GetAsync("system/datastores/"); + var datastoreRawResStr = await datastoreRawRes.Content.ReadAsStringAsync(); + var datastoreRes = JsonConvert.DeserializeObject>(datastoreRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreRawRes.StatusCode, $"Unexpected status code from GET {datastoreRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreRes.Count >= 1); + + var datastoreTestRawRes = await _httpClient.PostAsJsonAsync("system/datastores/test", datastoreRes[0]); + var datastoreTestRawResStr = await datastoreTestRawRes.Content.ReadAsStringAsync(); + var datastoreTestRes = JsonConvert.DeserializeObject>(datastoreTestRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreTestRawRes.StatusCode, $"Unexpected status code from POST {datastoreTestRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreTestRes.Count >= 1); + } + finally + { + await StopCertifyService(certifyService); + } + } + + [TestMethod, Description("Validate that Certify.Service.exe returns a valid response on route POST /api/system/datastores/update")] + public async Task TestCertifyServiceDatastoresUpdateRoute() + { + var certifyService = await StartCertifyService(); + + try + { + var datastoreRawRes = await _httpClient.GetAsync("system/datastores/"); + var datastoreRawResStr = await datastoreRawRes.Content.ReadAsStringAsync(); + var datastoreRes = JsonConvert.DeserializeObject>(datastoreRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreRawRes.StatusCode, $"Unexpected status code from GET {datastoreRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreRes.Count >= 1); + + var datastoreUpdateRawRes = await _httpClient.PostAsJsonAsync("system/datastores/update", datastoreRes[0]); + var datastoreUpdateRawResStr = await datastoreUpdateRawRes.Content.ReadAsStringAsync(); + var datastoreUpdateRes = JsonConvert.DeserializeObject>(datastoreUpdateRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreUpdateRawRes.StatusCode, $"Unexpected status code from POST {datastoreUpdateRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreUpdateRes.Count >= 1); + } + finally + { + await StopCertifyService(certifyService); + } + } + + [TestMethod, Description("Validate that Certify.Service.exe returns a valid response on route POST /api/system/datastores/setdefault/{dataStoreId}")] + public async Task TestCertifyServiceDatastoresSetDefaultRoute() + { + var certifyService = await StartCertifyService(); + + try + { + var datastoreRawRes = await _httpClient.GetAsync("system/datastores/"); + var datastoreRawResStr = await datastoreRawRes.Content.ReadAsStringAsync(); + var datastoreRes = JsonConvert.DeserializeObject>(datastoreRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreRawRes.StatusCode, $"Unexpected status code from GET {datastoreRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreRes.Count >= 1); + + var datastoreSetDefaultRawRes = await _httpClient.PostAsync($"system/datastores/setdefault/{datastoreRes[0].Id}", new StringContent("")); + var datastoreSetDefaultRawResStr = await datastoreSetDefaultRawRes.Content.ReadAsStringAsync(); + var datastoreSetDefaultRes = JsonConvert.DeserializeObject>(datastoreSetDefaultRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreSetDefaultRawRes.StatusCode, $"Unexpected status code from POST {datastoreSetDefaultRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreSetDefaultRes.Count >= 1); + } + finally + { + await StopCertifyService(certifyService); + } + } + + [TestMethod, Description("Validate that Certify.Service.exe returns a valid response on route POST /api/system/datastores/delete")] + [Ignore] + public async Task TestCertifyServiceDatastoresDeleteRoute() + { + var certifyService = await StartCertifyService(); + + try + { + var datastoreRawRes = await _httpClient.GetAsync("system/datastores/"); + var datastoreRawResStr = await datastoreRawRes.Content.ReadAsStringAsync(); + var datastoreRes = JsonConvert.DeserializeObject>(datastoreRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreRawRes.StatusCode, $"Unexpected status code from GET {datastoreRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreRes.Count >= 1); + + var datastoreDeleteRawRes = await _httpClient.PostAsync("system/datastores/delete", new StringContent(datastoreRes[0].Id)); + var datastoreDeleteRawResStr = await datastoreDeleteRawRes.Content.ReadAsStringAsync(); + var datastoreDeleteRes = JsonConvert.DeserializeObject>(datastoreDeleteRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreDeleteRawRes.StatusCode, $"Unexpected status code from POST {datastoreDeleteRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreDeleteRes.Count >= 1); + + var datastoreUpdateRawRes = await _httpClient.PostAsJsonAsync("system/datastores/update", datastoreRes[0]); + var datastoreUpdateRawResStr = await datastoreUpdateRawRes.Content.ReadAsStringAsync(); + var datastoreUpdateRes = JsonConvert.DeserializeObject>(datastoreUpdateRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreUpdateRawRes.StatusCode, $"Unexpected status code from POST {datastoreUpdateRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreUpdateRes.Count >= 1); + } + finally + { + await StopCertifyService(certifyService); + } + } + + [TestMethod, Description("Validate that Certify.Service.exe returns a valid response on route POST /api/system/datastores/copy/{sourceId}/{destId}")] + [Ignore] + public async Task TestCertifyServiceDatastoresCopyRoute() + { + var certifyService = await StartCertifyService(); + + try + { + var datastoreRawRes = await _httpClient.GetAsync("system/datastores/"); + var datastoreRawResStr = await datastoreRawRes.Content.ReadAsStringAsync(); + var datastoreRes = JsonConvert.DeserializeObject>(datastoreRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreRawRes.StatusCode, $"Unexpected status code from GET {datastoreRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreRes.Count >= 1); + + var newDataStoreId = "default-copy"; + var datastoreCopyRawRes = await _httpClient.PostAsync($"system/datastores/copy/{datastoreRes[0].Id}/{newDataStoreId}", new StringContent("")); + var datastoreCopyRawResStr = await datastoreCopyRawRes.Content.ReadAsStringAsync(); + var datastoreCopyRes = JsonConvert.DeserializeObject>(datastoreCopyRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreCopyRawRes.StatusCode, $"Unexpected status code from POST {datastoreCopyRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreCopyRes.Count >= 1); + + datastoreRawRes = await _httpClient.GetAsync("system/datastores/"); + datastoreRawResStr = await datastoreRawRes.Content.ReadAsStringAsync(); + datastoreRes = JsonConvert.DeserializeObject>(datastoreRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreRawRes.StatusCode, $"Unexpected status code from GET {datastoreRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreRes.Count >= 2); + + var datastoreDeleteRawRes = await _httpClient.PostAsJsonAsync("system/datastores/delete", newDataStoreId); + var datastoreDeleteRawResStr = await datastoreDeleteRawRes.Content.ReadAsStringAsync(); + var datastoreDeleteRes = JsonConvert.DeserializeObject>(datastoreDeleteRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, datastoreDeleteRawRes.StatusCode, $"Unexpected status code from POST {datastoreDeleteRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(datastoreDeleteRes.Count >= 1); + } + finally + { + await StopCertifyService(certifyService); + } + } + + [TestMethod, Description("Validate that Certify.Service.exe returns a valid response on route GET /api/server/isavailable/{serverType}")] + public async Task TestCertifyServiceServerIsavailableRoute() + { + var certifyService = await StartCertifyService(); + + try + { + var isAvailableRawRes = await _httpClient.GetAsync($"server/isavailable/{StandardServerTypes.IIS}"); + var isAvailableRawResStr = await isAvailableRawRes.Content.ReadAsStringAsync(); + var isAvailableRes = JsonConvert.DeserializeObject(isAvailableRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, isAvailableRawRes.StatusCode, $"Unexpected status code from GET {isAvailableRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + Assert.IsTrue(isAvailableRes); + } + finally + { + await StopCertifyService(certifyService); + } + } + + [TestMethod, Description("Validate that Certify.Service.exe returns a valid response on route GET /api/server/sitelist/{serverType}")] + public async Task TestCertifyServiceServerSitelistRoute() + { + var certifyService = await StartCertifyService(); + + try + { + var sitelistRawRes = await _httpClient.GetAsync($"server/sitelist/{StandardServerTypes.IIS}"); + var sitelistRawResStr = await sitelistRawRes.Content.ReadAsStringAsync(); + var sitelistRes = JsonConvert.DeserializeObject>(sitelistRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, sitelistRawRes.StatusCode, $"Unexpected status code from GET {sitelistRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + } + finally + { + await StopCertifyService(certifyService); + } + } + + [TestMethod, Description("Validate that Certify.Service.exe returns a valid response on route GET /api/server/version/{serverType}")] + public async Task TestCertifyServiceServerVersionRoute() + { + var certifyService = await StartCertifyService(); + + try + { + var versionRawRes = await _httpClient.GetAsync($"server/version/{StandardServerTypes.IIS}"); + var versionRawResStr = await versionRawRes.Content.ReadAsStringAsync(); + var versionRes = JsonConvert.DeserializeObject(versionRawResStr); + + Assert.AreEqual(HttpStatusCode.OK, versionRawRes.StatusCode, $"Unexpected status code from GET {versionRawRes.RequestMessage.RequestUri.AbsoluteUri}"); + StringAssert.Matches(versionRes, new Regex(@"^(\d+\.)?(\*|\d+)$"), $"Unexpected response from GET {versionRawRes.RequestMessage.RequestUri.AbsoluteUri} : {versionRawResStr}"); + } + finally + { + await StopCertifyService(certifyService); + } + } + } +#endif +} diff --git a/src/Certify.Tests/Certify.Core.Tests.Unit/ChallengeConfigMatchTests.cs b/src/Certify.Tests/Certify.Core.Tests.Unit/ChallengeConfigMatchTests.cs index 2053786f1..7ef2d122a 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Unit/ChallengeConfigMatchTests.cs +++ b/src/Certify.Tests/Certify.Core.Tests.Unit/ChallengeConfigMatchTests.cs @@ -144,8 +144,6 @@ public void ChallengeDelegationRuleTests() [TestMethod, Description("Ensure correct challenge config selected when rule is blank")] public void ChallengeDelegationRuleBlankRule() { - // wildcard rule tests [any subdomain source, any subdomain target] - var testRule = "*.test.com:*.auth.test.co.uk"; var result = Management.Challenges.DnsChallengeHelper.ApplyChallengeDelegationRule("test.com", "_acme-challenge.test.com", null); Assert.AreEqual("_acme-challenge.test.com", result); } diff --git a/src/Certify.Tests/Certify.Core.Tests.Unit/MiscTests.cs b/src/Certify.Tests/Certify.Core.Tests.Unit/MiscTests.cs index ec3a8506b..5ab938731 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Unit/MiscTests.cs +++ b/src/Certify.Tests/Certify.Core.Tests.Unit/MiscTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using Certify.Models.API; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -8,12 +8,6 @@ namespace Certify.Core.Tests.Unit [TestClass] public class MiscTests { - - public MiscTests() - { - - } - [TestMethod, Description("Test null/blank coalesce of string")] public void TestNullOrBlankCoalesce() { diff --git a/src/Certify.Tests/Certify.Core.Tests.Unit/RdapTests.cs b/src/Certify.Tests/Certify.Core.Tests.Unit/RdapTests.cs index 6e4d50931..cb6c25d63 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Unit/RdapTests.cs +++ b/src/Certify.Tests/Certify.Core.Tests.Unit/RdapTests.cs @@ -7,12 +7,6 @@ namespace Certify.Core.Tests.Unit [TestClass] public class RdapTests { - - public RdapTests() - { - - } - [TestMethod, Description("Test domain TLD check")] [DataTestMethod] [DataRow("example.com", "com")] diff --git a/src/Certify.Tests/Certify.Core.Tests.Unit/step-ca-win-init.bat b/src/Certify.Tests/Certify.Core.Tests.Unit/step-ca-win-init.bat index c2a262060..56fde2918 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Unit/step-ca-win-init.bat +++ b/src/Certify.Tests/Certify.Core.Tests.Unit/step-ca-win-init.bat @@ -1,23 +1,63 @@ -FOR /F "tokens=* USEBACKQ" %%F IN (`step path`) DO ( +FOR /F "tokens=* USEBACKQ" %%F IN (`step path`) DO ( SET STEPPATH=%%F ) +IF EXIST %STEPPATH%\config\ca.json ( + step-ca --password-file %STEPPATH%\secrets\password %STEPPATH%\config\ca.json + EXIT 0 +) + +IF "%DOCKER_STEPCA_INIT_NAME%"=="" ( + echo "there is no ca.json config file; please run step ca init, or provide config parameters via DOCKER_STEPCA_INIT_ vars" + EXIT 1 +) + +IF "%DOCKER_STEPCA_INIT_DNS_NAMES%"=="" ( + echo "there is no ca.json config file; please run step ca init, or provide config parameters via DOCKER_STEPCA_INIT_ vars" + EXIT 1 +) + +IF "%DOCKER_STEPCA_INIT_PROVISIONER_NAME%"=="" SET DOCKER_STEPCA_INIT_PROVISIONER_NAME=admin +IF "%DOCKER_STEPCA_INIT_ADMIN_SUBJECT%"=="" SET DOCKER_STEPCA_INIT_ADMIN_SUBJECT=step +IF "%DOCKER_STEPCA_INIT_ADDRESS%"=="" SET DOCKER_STEPCA_INIT_ADDRESS=:9000 + +IF NOT "%DOCKER_STEPCA_INIT_PASSWORD%"=="" ( +pwsh -Command "Out-File -FilePath "$Env:STEPPATH\password" -InputObject "$Env:DOCKER_STEPCA_INIT_PASSWORD";"^ + "Out-File -FilePath "$Env:STEPPATH\provisioner_password" -InputObject "$Env:DOCKER_STEPCA_INIT_PASSWORD";" +) ELSE IF NOT "%DOCKER_STEPCA_INIT_PASSWORD_FILE%"=="" ( +pwsh -Command "Out-File -FilePath "$Env:STEPPATH\password" -InputObject "$Env:DOCKER_STEPCA_INIT_PASSWORD_FILE";"^ + "Out-File -FilePath "$Env:STEPPATH\provisioner_password" -InputObject "$Env:DOCKER_STEPCA_INIT_PASSWORD_FILE";" +) ELSE ( pwsh -Command "$psw = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'.tochararray() | Get-Random -Count 40 | Join-String;"^ - "echo $psw;"^ "Out-File -FilePath "$Env:STEPPATH\password" -InputObject $psw;"^ "Out-File -FilePath "$Env:STEPPATH\provisioner_password" -InputObject $psw;"^ "Remove-Variable psw" +) + +setlocal + +SET INIT_ARGS=--deployment-type standalone --name %DOCKER_STEPCA_INIT_NAME% --dns %DOCKER_STEPCA_INIT_DNS_NAMES% --provisioner %DOCKER_STEPCA_INIT_PROVISIONER_NAME% --password-file %STEPPATH%\password --provisioner-password-file %STEPPATH%\provisioner_password --address %DOCKER_STEPCA_INIT_ADDRESS% + +IF "%DOCKER_STEPCA_INIT_SSH%"=="true" SET INIT_ARGS=%INIT_ARGS% -ssh +IF "%DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT%"=="true" SET INIT_ARGS=%INIT_ARGS% --remote-management --admin-subject %DOCKER_STEPCA_INIT_ADMIN_SUBJECT% + +step ca init %INIT_ARGS% +SET /p psw=<%STEPPATH%\provisioner_password +echo "👉 Your CA administrative password is: %psw%" +echo "🤫 This will only be displayed once." + +endlocal -step ca init --deployment-type standalone --name Smallstep --dns localhost --provisioner admin ^ ---password-file %STEPPATH%\password --provisioner-password-file %STEPPATH%\provisioner_password ^ ---address :9000 --remote-management --admin-subject step +SET HEALTH_URL=https://%DOCKER_STEPCA_INIT_DNS_NAMES%%DOCKER_STEPCA_INIT_ADDRESS%/health sdelete64 -accepteula -nobanner -q %STEPPATH%\provisioner_password move "%STEPPATH%\password" "%STEPPATH%\secrets\password" +:: Current error with running this program in Windows Docker Container causes issue reading DB first time, so they must be deleted to be recreated rmdir /s /q %STEPPATH%\db -step ca provisioner add acme --type ACME +:: Current error with running this program in Windows Docker Container causes ACME not to be set with --acme +IF "%DOCKER_STEPCA_INIT_ACME%"=="true" step ca provisioner add acme --type ACME step-ca --password-file %STEPPATH%\secrets\password %STEPPATH%\config\ca.json diff --git a/src/Certify.Tests/Certify.Core.Tests.Unit/step-ca-win.dockerfile b/src/Certify.Tests/Certify.Core.Tests.Unit/step-ca-win.dockerfile index 37cba87d9..e653a1724 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Unit/step-ca-win.dockerfile +++ b/src/Certify.Tests/Certify.Core.Tests.Unit/step-ca-win.dockerfile @@ -16,6 +16,6 @@ FROM base AS final COPY ./step-ca-win-init.bat . COPY --from=netapi /Windows/System32/netapi32.dll /Windows/System32/netapi32.dll -HEALTHCHECK CMD curl -Method GET -f http://localhost:9000/health || exit 1 +HEALTHCHECK CMD curl -Method GET -f %HEALTH_URL% || exit 1 CMD step-ca-win-init.bat && cmd diff --git a/src/Certify.Tests/Certify.Core.Tests.Unit/unit-test-462.runsettings b/src/Certify.Tests/Certify.Core.Tests.Unit/unit-test-462.runsettings new file mode 100644 index 000000000..9e1f06a10 --- /dev/null +++ b/src/Certify.Tests/Certify.Core.Tests.Unit/unit-test-462.runsettings @@ -0,0 +1,48 @@ + + + + + + %SystemDrive%\%HOMEPATH%\.nuget\packages\microsoft.codecoverage\17.8.0\build\netstandard2.0 + net462 + .\TestResults-4_6_2-windows + true + + + + + + + Cobertura + + + + + .*Certify.*$ + .*Plugin.Datastore.SQLite.dll$ + + + .*Certify.Core.Tests.Unit.dll$ + .*Moq.dll$ + + + + True + + + + + + + + + + + normal + + + + + + diff --git a/src/Certify.Tests/Certify.Core.Tests.Unit/unit-test-8-0-linux.runsettings b/src/Certify.Tests/Certify.Core.Tests.Unit/unit-test-8-0-linux.runsettings new file mode 100644 index 000000000..1ffd818a7 --- /dev/null +++ b/src/Certify.Tests/Certify.Core.Tests.Unit/unit-test-8-0-linux.runsettings @@ -0,0 +1,36 @@ + + + + + + $HOME/.nuget/packages/coverlet.collector/6.0.0/build/netstandard1.0;$HOME/.nuget/packages/coverlet.msbuild/6.0.0/build + net8.0 + ./TestResults-8_0-Linux + true + + + + + + + cobertura + + [Certify.*]*,[Plugin.Datastore.SQLite]* + + false + + + + + + + + + + normal + + + + + + diff --git a/src/Certify.Tests/Certify.Core.Tests.Unit/unit-test-8-0.runsettings b/src/Certify.Tests/Certify.Core.Tests.Unit/unit-test-8-0.runsettings new file mode 100644 index 000000000..1f1211437 --- /dev/null +++ b/src/Certify.Tests/Certify.Core.Tests.Unit/unit-test-8-0.runsettings @@ -0,0 +1,36 @@ + + + + + + %SystemDrive%\%HOMEPATH%\.nuget\packages\coverlet.collector\6.0.0\build\netstandard1.0;%SystemDrive%\%HOMEPATH%\.nuget\packages\coverlet.msbuild\6.0.0\build + net8.0 + .\TestResults-8_0-windows + true + + + + + + + cobertura + + [Certify.*]*,[Plugin.Datastore.SQLite]* + + false + + + + + + + + + + normal + + + + + + diff --git a/src/Certify.Tests/Certify.Core.Tests.Unit/windows_compose.yaml b/src/Certify.Tests/Certify.Core.Tests.Unit/windows_compose.yaml index 80e4f500c..8dd660ff5 100644 --- a/src/Certify.Tests/Certify.Core.Tests.Unit/windows_compose.yaml +++ b/src/Certify.Tests/Certify.Core.Tests.Unit/windows_compose.yaml @@ -50,6 +50,11 @@ services: profiles: ["4_6_2", "8_0"] ports: - 9000:9000 + environment: + DOCKER_STEPCA_INIT_NAME: Smallstep + DOCKER_STEPCA_INIT_DNS_NAMES: localhost + DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT: true + DOCKER_STEPCA_INIT_ACME: true volumes: - step:C:\Users\ContainerUser\.step