diff --git a/.github/workflows/android_e2e_tests.yml b/.github/workflows/android_e2e_tests.yml
index 7c7327dde..2f6730acf 100644
--- a/.github/workflows/android_e2e_tests.yml
+++ b/.github/workflows/android_e2e_tests.yml
@@ -135,27 +135,57 @@ jobs:
run: |
nohup appium --log appium.log &
- - name: Set up Docker (Ubuntu)
- if: runner.os == 'Linux'
- run: |
- # Check Docker version to ensure it's installed properly
- docker --version
- docker info
-
- # Allow current user to access Docker without sudo (optional, for convenience)
- sudo usermod -aG docker $USER
-
- # Restart Docker service to apply changes
- sudo systemctl restart docker
-
- name: Restore MAUI App for Android
run: dotnet restore TransactionProcessor.Mobile.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json
- name: Build Code
run: dotnet build TransactionProcessor.Mobile/TransactionProcessor.Mobile.csproj -f net10.0-android -c Release --no-restore
+ - name: Locate Android APK
+ id: android_apk
+ run: |
+ apk_path=$(find "$GITHUB_WORKSPACE/TransactionProcessor.Mobile/bin/Release/net10.0-android" -type f -name '*-Signed.apk' | head -n 1)
+ if [ -z "$apk_path" ]; then
+ echo "Unable to find a signed APK under TransactionProcessor.Mobile/bin/Release/net10.0-android."
+ find "$GITHUB_WORKSPACE/TransactionProcessor.Mobile/bin/Release/net10.0-android" -type f -name '*.apk' -print
+ exit 1
+ fi
+
+ echo "UITEST_ANDROID_APP_PATH=$apk_path" >> "$GITHUB_ENV"
+ echo "apk_path=$apk_path" >> "$GITHUB_OUTPUT"
+
+ - name: Prepare Android UI test settings
+ id: android_settings
+ run: |
+ settings_path="$RUNNER_TEMP/android-ui-tests.runsettings"
+ cp TransactionProcessor.Mobile.UITests.Android.runsettings "$settings_path"
+
+ python3 - "$settings_path" "${{ steps.android_apk.outputs.apk_path }}" <<'PY'
+ from pathlib import Path
+ import re
+ import sys
+
+ settings_file = Path(sys.argv[1])
+ apk_path = sys.argv[2]
+ content = settings_file.read_text()
+ updated_content, replacements = re.subn(
+ r".*?",
+ f"{apk_path}",
+ content,
+ count=1,
+ flags=re.DOTALL,
+ )
+
+ if replacements != 1:
+ raise SystemExit("Expected exactly one UITEST_ANDROID_APP_PATH entry in the runsettings template.")
+
+ settings_file.write_text(updated_content)
+ PY
+
+ echo "runsettings_path=$settings_path" >> "$GITHUB_OUTPUT"
+
- name: Run Android Navigation Tests
- run: dotnet test TransactionProcessor.Mobile.UITests/TransactionProcessor.Mobile.UITests.csproj --filter "(Category=PRTest)&(Category=Android)" --no-restore
+ run: dotnet test TransactionProcessor.Mobile.UITests/TransactionProcessor.Mobile.UITests.csproj --filter "(Category=PRTest)&(Category=Android)" --no-restore --settings "${{ steps.android_settings.outputs.runsettings_path }}"
- name: Upload Logs on Failure
if: always()
diff --git a/.github/workflows/android_navigation_tests.yml b/.github/workflows/android_navigation_tests.yml
index cdf470f3a..f53c1a9c9 100644
--- a/.github/workflows/android_navigation_tests.yml
+++ b/.github/workflows/android_navigation_tests.yml
@@ -141,8 +141,51 @@ jobs:
- name: Build Code
run: dotnet build TransactionProcessor.Mobile/TransactionProcessor.Mobile.csproj -f net10.0-android -c Release --no-restore
+ - name: Locate Android APK
+ id: android_apk
+ run: |
+ apk_path=$(find "$GITHUB_WORKSPACE/TransactionProcessor.Mobile/bin/Release/net10.0-android" -type f -name '*-Signed.apk' | head -n 1)
+ if [ -z "$apk_path" ]; then
+ echo "Unable to find a signed APK under TransactionProcessor.Mobile/bin/Release/net10.0-android."
+ find "$GITHUB_WORKSPACE/TransactionProcessor.Mobile/bin/Release/net10.0-android" -type f -name '*.apk' -print
+ exit 1
+ fi
+
+ echo "UITEST_ANDROID_APP_PATH=$apk_path" >> "$GITHUB_ENV"
+ echo "apk_path=$apk_path" >> "$GITHUB_OUTPUT"
+
+ - name: Prepare Android UI test settings
+ id: android_settings
+ run: |
+ settings_path="$RUNNER_TEMP/android-ui-tests.runsettings"
+ cp TransactionProcessor.Mobile.UITests.Android.runsettings "$settings_path"
+
+ python3 - "$settings_path" "${{ steps.android_apk.outputs.apk_path }}" <<'PY'
+ from pathlib import Path
+ import re
+ import sys
+
+ settings_file = Path(sys.argv[1])
+ apk_path = sys.argv[2]
+ content = settings_file.read_text()
+ updated_content, replacements = re.subn(
+ r".*?",
+ f"{apk_path}",
+ content,
+ count=1,
+ flags=re.DOTALL,
+ )
+
+ if replacements != 1:
+ raise SystemExit("Expected exactly one UITEST_ANDROID_APP_PATH entry in the runsettings template.")
+
+ settings_file.write_text(updated_content)
+ PY
+
+ echo "runsettings_path=$settings_path" >> "$GITHUB_OUTPUT"
+
- name: Run Android Navigation Tests
- run: dotnet test TransactionProcessor.Mobile.UITests/TransactionProcessor.Mobile.UITests.csproj --filter "(Category=PRNavTest)&(Category=Android)" --no-restore
+ run: dotnet test TransactionProcessor.Mobile.UITests/TransactionProcessor.Mobile.UITests.csproj --filter "(Category=PRNavTest)&(Category=Android)" --no-restore --settings "${{ steps.android_settings.outputs.runsettings_path }}"
- name: Upload Appium Logs on Failure
if: always()
@@ -286,8 +329,51 @@ jobs:
- name: Build Code
run: dotnet build TransactionProcessor.Mobile/TransactionProcessor.Mobile.csproj -f net10.0-android -c Release --no-restore
+ - name: Locate Android APK
+ id: android_apk
+ run: |
+ apk_path=$(find "$GITHUB_WORKSPACE/TransactionProcessor.Mobile/bin/Release/net10.0-android" -type f -name '*-Signed.apk' | head -n 1)
+ if [ -z "$apk_path" ]; then
+ echo "Unable to find a signed APK under TransactionProcessor.Mobile/bin/Release/net10.0-android."
+ find "$GITHUB_WORKSPACE/TransactionProcessor.Mobile/bin/Release/net10.0-android" -type f -name '*.apk' -print
+ exit 1
+ fi
+
+ echo "UITEST_ANDROID_APP_PATH=$apk_path" >> "$GITHUB_ENV"
+ echo "apk_path=$apk_path" >> "$GITHUB_OUTPUT"
+
+ - name: Prepare Android UI test settings
+ id: android_settings
+ run: |
+ settings_path="$RUNNER_TEMP/android-ui-tests.runsettings"
+ cp TransactionProcessor.Mobile.UITests.Android.runsettings "$settings_path"
+
+ python3 - "$settings_path" "${{ steps.android_apk.outputs.apk_path }}" <<'PY'
+ from pathlib import Path
+ import re
+ import sys
+
+ settings_file = Path(sys.argv[1])
+ apk_path = sys.argv[2]
+ content = settings_file.read_text()
+ updated_content, replacements = re.subn(
+ r".*?",
+ f"{apk_path}",
+ content,
+ count=1,
+ flags=re.DOTALL,
+ )
+
+ if replacements != 1:
+ raise SystemExit("Expected exactly one UITEST_ANDROID_APP_PATH entry in the runsettings template.")
+
+ settings_file.write_text(updated_content)
+ PY
+
+ echo "runsettings_path=$settings_path" >> "$GITHUB_OUTPUT"
+
- name: Run Android Navigation Tests
- run: dotnet test TransactionProcessor.Mobile.UITests/TransactionProcessor.Mobile.UITests.csproj --filter "(Category=PRHWNavTest)&(Category=Android)" --no-restore
+ run: dotnet test TransactionProcessor.Mobile.UITests/TransactionProcessor.Mobile.UITests.csproj --filter "(Category=PRHWNavTest)&(Category=Android)" --no-restore --settings "${{ steps.android_settings.outputs.runsettings_path }}"
- name: Upload Appium Logs on Failure
if: always()
diff --git a/.github/workflows/windows_e2e_tests.yml b/.github/workflows/windows_e2e_tests.yml
index 44379f3e1..87d85c0f7 100644
--- a/.github/workflows/windows_e2e_tests.yml
+++ b/.github/workflows/windows_e2e_tests.yml
@@ -33,9 +33,6 @@ jobs:
- name: Restore MAUI App for Windows
run: dotnet restore TransactionProcessor.Mobile.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json
- - name: Build Code
- run: dotnet build TransactionProcessor.Mobile/TransactionProcessor.Mobile.csproj -f net10.0-android -c Release --no-restore
-
- name: Decrypt PFX File
run: |
echo "${{ secrets.WINDOWSSIGNINGCERT }}" > cert.pfx.asc
@@ -71,8 +68,57 @@ jobs:
Add-AppxPackage -Path $msixPackage.FullName -ForceApplicationShutdown
}
+ - name: Resolve Windows UI test app id
+ id: windows_app
+ shell: powershell
+ run: |
+ $package = $null
+ for ($attempt = 1; $attempt -le 12 -and $null -eq $package; $attempt++) {
+ $package = Get-AppxPackage -AllUsers |
+ Where-Object {
+ $_.Name -like '*TransactionMobile*' -or
+ $_.Name -like '*TransactionProcessor.Mobile*' -or
+ $_.PackageFamilyName -like '*TransactionMobile*' -or
+ $_.PackageFamilyName -like '*TransactionProcessor.Mobile*' -or
+ $_.InstallLocation -like '*TransactionProcessor.Mobile*'
+ } |
+ Sort-Object InstallDate -Descending |
+ Select-Object -First 1
+
+ if ($null -eq $package) {
+ Write-Host ("Attempt {0}: installed app not visible yet, waiting 5 seconds." -f $attempt)
+ Start-Sleep -Seconds 5
+ }
+ }
+
+ if ($null -eq $package) {
+ Write-Error "Unable to find the installed TransactionProcessor.Mobile package."
+ Get-AppxPackage -AllUsers | Sort-Object Name | Select-Object Name, PackageFamilyName, InstallLocation | Format-Table -AutoSize
+ exit 1
+ }
+
+ $windowsAppId = "$($package.PackageFamilyName)!App"
+ Write-Host "Resolved Windows app id: $windowsAppId"
+ "windows_app_id=$windowsAppId" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
+
+ - name: Prepare Windows UI test settings
+ id: windows_settings
+ shell: powershell
+ run: |
+ $settingsPath = Join-Path $env:RUNNER_TEMP "windows-ui-tests.runsettings"
+ Copy-Item -Path "TransactionProcessor.Mobile.UITests.Windows.runsettings" -Destination $settingsPath -Force
+ [xml]$settingsXml = Get-Content -Raw $settingsPath
+ $nodes = $settingsXml.SelectNodes("//EnvironmentVariables/UITEST_WINDOWS_APP_ID")
+ if ($nodes.Count -ne 1) {
+ throw "Expected exactly one UITEST_WINDOWS_APP_ID entry in the runsettings template."
+ }
+
+ $nodes.Item(0).InnerText = "${{ steps.windows_app.outputs.windows_app_id }}"
+ $settingsXml.Save($settingsPath)
+ "runsettings_path=$settingsPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
+
- name: Run Windows End To End Tests
- run: dotnet test TransactionProcessor.Mobile.UiTests/TransactionProcessor.Mobile.UiTests.csproj --filter "Category=PRTest&Category=Windows" --no-restore
+ run: dotnet test TransactionProcessor.Mobile.UITests/TransactionProcessor.Mobile.UITests.csproj --filter "Category=PRTest&Category=Windows" --no-restore --settings "${{ steps.windows_settings.outputs.runsettings_path }}"
#run: dotnet test TransactionProcessor.Mobile.UiTests/TransactionProcessor.Mobile.UiTests.csproj --list-tests -v:d
- name: Upload Logs on Failure
diff --git a/.github/workflows/windows_navigation_tests.yml b/.github/workflows/windows_navigation_tests.yml
index eece68666..7c6162ccc 100644
--- a/.github/workflows/windows_navigation_tests.yml
+++ b/.github/workflows/windows_navigation_tests.yml
@@ -31,10 +31,7 @@ jobs:
nohup appium --log appium.log &
- name: Restore MAUI App for Windows
- run: dotnet restore TransactionProcessor.Mobile.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json
-
- - name: Build Code
- run: dotnet build TransactionProcessor.Mobile/TransactionProcessor.Mobile.csproj -f net10.0-android -c Release --no-restore
+ run: dotnet restore TransactionProcessor.Mobile.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json
- name: Decrypt PFX File
run: |
@@ -86,8 +83,57 @@ jobs:
Add-AppxPackage -Path $msixPackage.FullName -ForceApplicationShutdown
}
+ - name: Resolve Windows UI test app id
+ id: windows_app
+ shell: powershell
+ run: |
+ $package = $null
+ for ($attempt = 1; $attempt -le 12 -and $null -eq $package; $attempt++) {
+ $package = Get-AppxPackage -AllUsers |
+ Where-Object {
+ $_.Name -like '*TransactionMobile*' -or
+ $_.Name -like '*TransactionProcessor.Mobile*' -or
+ $_.PackageFamilyName -like '*TransactionMobile*' -or
+ $_.PackageFamilyName -like '*TransactionProcessor.Mobile*' -or
+ $_.InstallLocation -like '*TransactionProcessor.Mobile*'
+ } |
+ Sort-Object InstallDate -Descending |
+ Select-Object -First 1
+
+ if ($null -eq $package) {
+ Write-Host ("Attempt {0}: installed app not visible yet, waiting 5 seconds." -f $attempt)
+ Start-Sleep -Seconds 5
+ }
+ }
+
+ if ($null -eq $package) {
+ Write-Error "Unable to find the installed TransactionProcessor.Mobile package."
+ Get-AppxPackage -AllUsers | Sort-Object Name | Select-Object Name, PackageFamilyName, InstallLocation | Format-Table -AutoSize
+ exit 1
+ }
+
+ $windowsAppId = "$($package.PackageFamilyName)!App"
+ Write-Host "Resolved Windows app id: $windowsAppId"
+ "windows_app_id=$windowsAppId" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
+
+ - name: Prepare Windows UI test settings
+ id: windows_settings
+ shell: powershell
+ run: |
+ $settingsPath = Join-Path $env:RUNNER_TEMP "windows-ui-tests.runsettings"
+ Copy-Item -Path "TransactionProcessor.Mobile.UITests.Windows.runsettings" -Destination $settingsPath -Force
+ [xml]$settingsXml = Get-Content -Raw $settingsPath
+ $nodes = $settingsXml.SelectNodes("//EnvironmentVariables/UITEST_WINDOWS_APP_ID")
+ if ($nodes.Count -ne 1) {
+ throw "Expected exactly one UITEST_WINDOWS_APP_ID entry in the runsettings template."
+ }
+
+ $nodes.Item(0).InnerText = "${{ steps.windows_app.outputs.windows_app_id }}"
+ $settingsXml.Save($settingsPath)
+ "runsettings_path=$settingsPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
+
- name: Run Windows Navigation Tests
- run: dotnet test TransactionProcessor.Mobile.UiTests/TransactionProcessor.Mobile.UiTests.csproj --filter "(Category=PRNavTest)&(Category=Windows)" --no-restore
+ run: dotnet test TransactionProcessor.Mobile.UITests/TransactionProcessor.Mobile.UITests.csproj --filter "(Category=PRNavTest)&(Category=Windows)" --no-restore --settings "${{ steps.windows_settings.outputs.runsettings_path }}"
- name: Upload Appium Logs on Failure
if: always()
diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/AuthenticationService.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/AuthenticationService.cs
index 02576301f..179d6345d 100644
--- a/TransactionProcessor.Mobile.BusinessLogic/Services/AuthenticationService.cs
+++ b/TransactionProcessor.Mobile.BusinessLogic/Services/AuthenticationService.cs
@@ -1,4 +1,4 @@
-using SecurityService.Client;
+using SecurityService.Client;
using SecurityService.DataTransferObjects;
using Shared.Results;
using SimpleResults;
@@ -105,4 +105,4 @@ public async Task> RefreshAccessToken(String refreshT
}
}
}
-}
\ No newline at end of file
+}
diff --git a/TransactionProcessor.Mobile.UITests.Android.runsettings b/TransactionProcessor.Mobile.UITests.Android.runsettings
new file mode 100644
index 000000000..955e68ffa
--- /dev/null
+++ b/TransactionProcessor.Mobile.UITests.Android.runsettings
@@ -0,0 +1,10 @@
+
+
+
+
+ Android
+ G:\Git\TransactionProcessing\TransactionMobile\TransactionProcessor.Mobile\bin\Debug\net10.0-android\com.transactionprocessor.mobile-Signed.apk
+ 180
+
+
+
diff --git a/TransactionProcessor.Mobile.UITests.Windows.runsettings b/TransactionProcessor.Mobile.UITests.Windows.runsettings
new file mode 100644
index 000000000..c8c86f010
--- /dev/null
+++ b/TransactionProcessor.Mobile.UITests.Windows.runsettings
@@ -0,0 +1,10 @@
+
+
+
+
+ Windows
+ TransactionMobile_zct748q4xfh0m!App
+ 180
+
+
+
diff --git a/TransactionProcessor.Mobile.UITests/Common/GenericSteps.cs b/TransactionProcessor.Mobile.UITests/Common/GenericSteps.cs
index a2bc83eb8..39fc2f79f 100644
--- a/TransactionProcessor.Mobile.UITests/Common/GenericSteps.cs
+++ b/TransactionProcessor.Mobile.UITests/Common/GenericSteps.cs
@@ -1,7 +1,7 @@
-using NLog;
+using NLog;
using Reqnroll;
-using Shared.IntegrationTesting;
using Shared.Logger;
+using TransactionProcessor.Mobile.UITests.Drivers;
namespace TransactionProcessor.Mobile.UITests.Common;
@@ -9,81 +9,66 @@ namespace TransactionProcessor.Mobile.UITests.Common;
[Scope(Tag = "base")]
public class GenericSteps
{
- private readonly ScenarioContext ScenarioContext;
-
- private readonly TestingContext TestingContext;
+ private readonly ScenarioContext scenarioContext;
+ private readonly TestingContext testingContext;
+ private readonly AppiumDriverWrapper appiumDriver;
public GenericSteps(ScenarioContext scenarioContext,
- TestingContext testingContext)
+ TestingContext testingContext,
+ AppiumDriverWrapper appiumDriver)
{
- this.ScenarioContext = scenarioContext;
- this.TestingContext = testingContext;
+ this.scenarioContext = scenarioContext;
+ this.testingContext = testingContext;
+ this.appiumDriver = appiumDriver;
}
[BeforeScenario(Order = 0)]
- public async Task StartSystem(){
- if (this.ScenarioContext.ScenarioInfo.Tags.Contains("PRNavTest") ||
- this.ScenarioContext.ScenarioInfo.Tags.Contains("PRHWNavTest"))
- {
- // Initialise a logger
- String scenarioName = this.ScenarioContext.ScenarioInfo.Title.Replace(" ", "");
- NlogLogger logger = new NlogLogger();
- logger.Initialise(LogManager.GetLogger(scenarioName), scenarioName);
- LogManager.AddHiddenAssembly(typeof(NlogLogger).Assembly);
- this.TestingContext.Logger = logger;
- }
- else{
- DockerServices dockerServices = DockerServices.EventStore |
- DockerServices.MessagingService | DockerServices.SecurityService |
- DockerServices.TestHost | DockerServices.SqlServer | DockerServices.TransactionProcessor |
- DockerServices.TransactionProcessorAcl | DockerServices.ConfigurationHost;
-
- //dockerServices = DockerServices.EventStore | DockerServices.SqlServer | (DockerServices)512;
-
- // Initialise a logger
- String scenarioName = this.ScenarioContext.ScenarioInfo.Title.Replace(" ", "");
- NlogLogger logger = new NlogLogger();
- logger.Initialise(LogManager.GetLogger(scenarioName), scenarioName);
- LogManager.AddHiddenAssembly(typeof(NlogLogger).Assembly);
-
- this.TestingContext.DockerHelper = new DockerHelper();
- this.TestingContext.DockerHelper.Logger = logger;
- this.TestingContext.Logger = logger;
- this.TestingContext.DockerHelper.RequiredDockerServices = dockerServices;
- this.TestingContext.Logger.LogInformation("About to Start Global Setup");
-
- String? isCi = Environment.GetEnvironmentVariable("IsCI");
- this.TestingContext.Logger.LogInformation($"IsCI [{isCi}]");
- if (String.Compare(isCi, Boolean.TrueString, StringComparison.InvariantCultureIgnoreCase) == 0)
- {
- // override teh SQL Server image
- this.TestingContext.Logger.LogInformation("Sql Image overridden");
- this.TestingContext.DockerHelper.SetImageDetails(ContainerType.SqlServer, ("mssqlserver:2022-ltsc2022", false));
- }
-
- await Setup.GlobalSetup(this.TestingContext.DockerHelper);
-
- this.TestingContext.DockerHelper.DockerCredentials = Setup.DockerCredentials;
- this.TestingContext.DockerHelper.SqlCredentials = Setup.SqlCredentials;
- this.TestingContext.DockerHelper.SqlServerContainerName = "sharedsqlserver";
+ public async Task StartSystem()
+ {
+ string scenarioName = this.scenarioContext.ScenarioInfo.Title.Replace(" ", "");
+ NlogLogger logger = new();
+ logger.Initialise(LogManager.GetLogger(scenarioName), scenarioName);
+ LogManager.AddHiddenAssembly(typeof(NlogLogger).Assembly);
- this.TestingContext.Logger.LogInformation("About to Start Containers for Scenario Run");
+ this.testingContext.Logger = logger;
+ this.testingContext.TestHostHelper = new TestHostHelper(this.testingContext);
+ this.testingContext.TestHostHelper.Logger = logger;
-
+ try
+ {
+ this.testingContext.Logger.LogInformation("About to start local test host");
+ await Setup.GlobalSetup().ConfigureAwait(false);
+ await this.testingContext.TestHostHelper.StartTestHostForScenarioRun(scenarioName).ConfigureAwait(false);
+ this.testingContext.Logger.LogInformation("Local test host started");
+ }
+ catch
+ {
+ await UiFailureDiagnostics.CaptureAsync(this.testingContext, this.scenarioContext, this.appiumDriver, "BeforeScenario").ConfigureAwait(false);
+ throw;
+ }
+ }
- await this.TestingContext.DockerHelper.StartContainersForScenarioRun(scenarioName, dockerServices).ConfigureAwait(false);
- this.TestingContext.Logger.LogInformation("Containers for Scenario Run Started");
+ [AfterStep(Order = 100)]
+ public async Task CaptureFailureDiagnosticsAfterStep()
+ {
+ if (this.scenarioContext.TestError == null)
+ {
+ return;
}
+
+ await UiFailureDiagnostics.CaptureAsync(this.testingContext, this.scenarioContext, this.appiumDriver, "AfterStep").ConfigureAwait(false);
}
[AfterScenario(Order = 0)]
public async Task StopSystem()
{
- if (this.ScenarioContext.ScenarioInfo.Tags.Contains("PRNavTest") == false && this.ScenarioContext.ScenarioInfo.Tags.Contains("PRHWNavTest") == false)
+ this.testingContext.Logger?.LogInformation("About to stop local test host");
+ if (this.testingContext.TestHostHelper != null)
{
- this.TestingContext.Logger.LogInformation("About to Stop Containers for Scenario Run");
- await this.TestingContext.DockerHelper.StopContainersForScenarioRun(DockerServices.None).ConfigureAwait(false);
- this.TestingContext.Logger.LogInformation("Containers for Scenario Run Stopped");
+ await this.testingContext.TestHostHelper.StopTestHostForScenarioRun().ConfigureAwait(false);
}
+
+ this.testingContext.Logger?.LogInformation("Local test host stopped");
}
-}
\ No newline at end of file
+}
+
diff --git a/TransactionProcessor.Mobile.UITests/Common/Setup.cs b/TransactionProcessor.Mobile.UITests/Common/Setup.cs
index c31d00a7a..f3caa5613 100644
--- a/TransactionProcessor.Mobile.UITests/Common/Setup.cs
+++ b/TransactionProcessor.Mobile.UITests/Common/Setup.cs
@@ -1,5 +1,3 @@
-using Ductus.FluentDocker.Services;
-using Ductus.FluentDocker.Services.Extensions;
using Reqnroll;
using Shouldly;
@@ -7,13 +5,13 @@ namespace TransactionProcessor.Mobile.UITests.Common
{
[Binding]
[Scope(Tag = "base")]
- public class Setup{
- public static (String usename, String password) SqlCredentials = ("sa", "thisisalongpassword123!");
- public static (String url, String username, String password) DockerCredentials = ("https://www.docker.com", "stuartferguson", "Sc0tland");
+ public class Setup
+ {
//[BeforeTestRun]
- public static async Task GlobalSetup(DockerHelper dockerHelper)
+ public static Task GlobalSetup()
{
ShouldlyConfiguration.DefaultTaskTimeout = TimeSpan.FromMinutes(1);
+ return Task.CompletedTask;
}
}
}
diff --git a/TransactionProcessor.Mobile.UITests/Common/TestAppConfig.cs b/TransactionProcessor.Mobile.UITests/Common/TestAppConfig.cs
new file mode 100644
index 000000000..427766534
--- /dev/null
+++ b/TransactionProcessor.Mobile.UITests/Common/TestAppConfig.cs
@@ -0,0 +1,111 @@
+using System;
+using TransactionProcessor.Mobile.UITests.Drivers;
+
+namespace TransactionProcessor.Mobile.UITests.Common;
+
+public sealed record TestAppConfig
+{
+ public enum AppiumTraceMode
+ {
+ Summary,
+ Verbose,
+ Off,
+ }
+
+ public MobileTestPlatform Platform { get; init; }
+
+ public string? AndroidAppPath { get; init; }
+
+ public string? WindowsAppId { get; init; }
+
+ public TimeSpan AppiumStartupTimeout { get; init; } = TimeSpan.FromSeconds(180);
+
+ public AppiumTraceMode AppiumTrace { get; init; } = AppiumTraceMode.Summary;
+
+ public static TestAppConfig Load(MobileTestPlatform defaultPlatform)
+ {
+ MobileTestPlatform platform = ResolvePlatform(defaultPlatform);
+ string? androidAppPath = Environment.GetEnvironmentVariable("UITEST_ANDROID_APP_PATH");
+ string? windowsAppId = Environment.GetEnvironmentVariable("UITEST_WINDOWS_APP_ID");
+ TimeSpan appiumStartupTimeout = ResolveAppiumStartupTimeout();
+ AppiumTraceMode appiumTrace = ResolveAppiumTraceMode();
+
+ return platform switch
+ {
+ MobileTestPlatform.Android => new TestAppConfig
+ {
+ Platform = platform,
+ AndroidAppPath = RequireValue(androidAppPath, "UITEST_ANDROID_APP_PATH"),
+ AppiumStartupTimeout = appiumStartupTimeout,
+ AppiumTrace = appiumTrace
+ },
+ MobileTestPlatform.Windows => new TestAppConfig
+ {
+ Platform = platform,
+ WindowsAppId = RequireValue(windowsAppId, "UITEST_WINDOWS_APP_ID"),
+ AppiumStartupTimeout = appiumStartupTimeout,
+ AppiumTrace = appiumTrace
+ },
+ _ => throw new InvalidOperationException($"Unsupported platform '{platform}'."),
+ };
+ }
+
+ private static AppiumTraceMode ResolveAppiumTraceMode()
+ {
+ string? traceModeValue = Environment.GetEnvironmentVariable("UITEST_APPIUM_TRACE_MODE");
+ if (string.IsNullOrWhiteSpace(traceModeValue))
+ {
+ return AppiumTraceMode.Summary;
+ }
+
+ if (Enum.TryParse(traceModeValue, ignoreCase: true, out AppiumTraceMode parsed) == false)
+ {
+ throw new InvalidOperationException(
+ $"Invalid UITEST_APPIUM_TRACE_MODE value '{traceModeValue}'. Expected Off, Summary, or Verbose.");
+ }
+
+ return parsed;
+ }
+
+ private static TimeSpan ResolveAppiumStartupTimeout()
+ {
+ string? timeoutValue = Environment.GetEnvironmentVariable("UITEST_APPIUM_STARTUP_TIMEOUT_SECONDS");
+ if (string.IsNullOrWhiteSpace(timeoutValue))
+ {
+ return TimeSpan.FromSeconds(180);
+ }
+
+ if (int.TryParse(timeoutValue, out int timeoutSeconds) == false || timeoutSeconds <= 0)
+ {
+ throw new InvalidOperationException($"Invalid UITEST_APPIUM_STARTUP_TIMEOUT_SECONDS value '{timeoutValue}'. Expected a positive whole number of seconds.");
+ }
+
+ return TimeSpan.FromSeconds(timeoutSeconds);
+ }
+
+ private static MobileTestPlatform ResolvePlatform(MobileTestPlatform defaultPlatform)
+ {
+ string? platformValue = Environment.GetEnvironmentVariable("UITEST_PLATFORM");
+ if (string.IsNullOrWhiteSpace(platformValue))
+ {
+ return defaultPlatform;
+ }
+
+ if (Enum.TryParse(platformValue, ignoreCase: true, out MobileTestPlatform parsed) == false)
+ {
+ throw new InvalidOperationException($"Invalid UITEST_PLATFORM value '{platformValue}'. Expected Android or Windows.");
+ }
+
+ return parsed;
+ }
+
+ private static string RequireValue(string? value, string variableName)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ throw new InvalidOperationException($"Missing required environment variable '{variableName}'.");
+ }
+
+ return value;
+ }
+}
diff --git a/TransactionProcessor.Mobile.UITests/Common/TestAppConfigTests.cs b/TransactionProcessor.Mobile.UITests/Common/TestAppConfigTests.cs
new file mode 100644
index 000000000..b74542a53
--- /dev/null
+++ b/TransactionProcessor.Mobile.UITests/Common/TestAppConfigTests.cs
@@ -0,0 +1,59 @@
+using System;
+using NUnit.Framework;
+using Shouldly;
+using TransactionProcessor.Mobile.UITests.Drivers;
+
+namespace TransactionProcessor.Mobile.UITests.Common;
+
+[TestFixture]
+[NonParallelizable]
+public class TestAppConfigTests
+{
+ private string? originalPlatform;
+ private string? originalAndroidAppPath;
+ private string? originalWindowsAppId;
+
+ [SetUp]
+ public void CaptureEnvironment()
+ {
+ this.originalPlatform = Environment.GetEnvironmentVariable("UITEST_PLATFORM");
+ this.originalAndroidAppPath = Environment.GetEnvironmentVariable("UITEST_ANDROID_APP_PATH");
+ this.originalWindowsAppId = Environment.GetEnvironmentVariable("UITEST_WINDOWS_APP_ID");
+ }
+
+ [TearDown]
+ public void RestoreEnvironment()
+ {
+ Environment.SetEnvironmentVariable("UITEST_PLATFORM", this.originalPlatform);
+ Environment.SetEnvironmentVariable("UITEST_ANDROID_APP_PATH", this.originalAndroidAppPath);
+ Environment.SetEnvironmentVariable("UITEST_WINDOWS_APP_ID", this.originalWindowsAppId);
+ }
+
+ [Test]
+ public void Load_AndroidUsesAndroidAppPath()
+ {
+ Environment.SetEnvironmentVariable("UITEST_PLATFORM", "Android");
+ Environment.SetEnvironmentVariable("UITEST_ANDROID_APP_PATH", @"C:\apps\TransactionProcessor.Mobile.apk");
+ Environment.SetEnvironmentVariable("UITEST_WINDOWS_APP_ID", null);
+
+ TestAppConfig config = TestAppConfig.Load(MobileTestPlatform.Windows);
+
+ config.Platform.ShouldBe(MobileTestPlatform.Android);
+ config.AndroidAppPath.ShouldBe(@"C:\apps\TransactionProcessor.Mobile.apk");
+ config.WindowsAppId.ShouldBeNull();
+ }
+
+ [Test]
+ public void Load_WindowsUsesWindowsAppId()
+ {
+ Environment.SetEnvironmentVariable("UITEST_PLATFORM", "Windows");
+ Environment.SetEnvironmentVariable("UITEST_ANDROID_APP_PATH", null);
+ Environment.SetEnvironmentVariable("UITEST_WINDOWS_APP_ID", "TransactionMobile_123!App");
+
+ TestAppConfig config = TestAppConfig.Load(MobileTestPlatform.Android);
+
+ config.Platform.ShouldBe(MobileTestPlatform.Windows);
+ config.WindowsAppId.ShouldBe("TransactionMobile_123!App");
+ config.AndroidAppPath.ShouldBeNull();
+ }
+}
diff --git a/TransactionProcessor.Mobile.UITests/Common/DockerHelper.cs b/TransactionProcessor.Mobile.UITests/Common/TestHostHelper.cs
similarity index 54%
rename from TransactionProcessor.Mobile.UITests/Common/DockerHelper.cs
rename to TransactionProcessor.Mobile.UITests/Common/TestHostHelper.cs
index 1143277cc..b31ecfda0 100644
--- a/TransactionProcessor.Mobile.UITests/Common/DockerHelper.cs
+++ b/TransactionProcessor.Mobile.UITests/Common/TestHostHelper.cs
@@ -15,209 +15,144 @@
using TransactionProcessor.Client;
using TransactionProcessor.DataTransferObjects.Responses.Contract;
using TransactionProcessor.IntegrationTesting.Helpers;
+using TransactionProcessor.Mobile.UiTestBackend;
+using TransactionProcessor.Mobile.UITests.Drivers;
using ReqnrollTableHelper = Shared.IntegrationTesting.ReqnrollTableHelper;
namespace TransactionProcessor.Mobile.UITests.Common
{
- public class DockerHelper : global::Shared.IntegrationTesting.TestContainers.DockerHelper
+ public class TestHostHelper
{
- #region Fields
-
- ///
- /// The HTTP client
- ///
- public HttpClient HttpClient;
+ private readonly HttpClientHandler httpClientHandler;
- ///
- /// The security service client
- ///
- public ISecurityServiceClient SecurityServiceClient;
+ public TestHostHelper(TestingContext testingContext)
+ {
+ StringSerialiser.Initialise((IStringSerialiser)new SystemTextJsonSerializer(SystemTextJsonSerializer.GetDefaultJsonSerializerOptions()));
+ this.TestingContext = testingContext;
+ this.httpClientHandler = new HttpClientHandler
+ {
+ ServerCertificateCustomValidationCallback = (_, _, _, _) => true
+ };
+ }
- ///
- /// The transaction processor client
- ///
- public ITransactionProcessorClient TransactionProcessorClient;
+ public TestingContext TestingContext { get; }
- private const String MinimumSupportedApplicationVersion = "1.0.5";
+ public Guid TestId { get; private set; } = Guid.NewGuid();
- private readonly TestingContext TestingContext;
+ public string LocalIPAddress { get; private set; } = "127.0.0.1";
- #endregion
+ public int ConfigHostPort { get; private set; }
- public EventStoreProjectionManagementClient ProjectionManagementClient;
+ public string ConfigHostName => $"mobileconfighost{this.TestId:N}";
- #region Constructors
+ public HttpClient? HttpClient { get; private set; }
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- public DockerHelper()
+ public HttpClient? TestHostHttpClient { get; private set; }
+
+ public ISecurityServiceClient? SecurityServiceClient { get; private set; }
+
+ public ITransactionProcessorClient? TransactionProcessorClient { get; private set; }
+
+ public EventStoreProjectionManagementClient? ProjectionManagementClient { get; private set; }
+
+ public NlogLogger? Logger { get; set; }
+
+ public BackendSeed ScenarioSeed => this.TestingContext.ScenarioSeed;
+
+ public async Task StartTestHostForScenarioRun(String scenarioName, object? _ = null)
{
- this.TestingContext = new TestingContext();
- StringSerialiser.Initialise((IStringSerialiser)new SystemTextJsonSerializer(SystemTextJsonSerializer.GetDefaultJsonSerializerOptions()));
- }
+ this.TestId = Guid.NewGuid();
+ this.LocalIPAddress = this.GetLocalIPAddress();
+ this.TestingContext.ResetScenarioState();
- #endregion
+ int port = GetFreeTcpPort();
+ this.ConfigHostPort = port;
- #region Methods
+ this.TestingContext.BackendHost = await TestBackendHost.StartAsync(port, this.TestingContext.ScenarioSeed).ConfigureAwait(false);
+ this.TestingContext.BackendHost.ApplySeed(this.TestingContext.ScenarioSeed);
- public string GetLocalIPAddress()
+ this.HttpClient = this.TestingContext.BackendHost.CreateClient();
+ this.TestHostHttpClient = this.TestingContext.BackendHost.CreateClient();
+ this.SecurityServiceClient = null;
+ this.TransactionProcessorClient = null;
+ this.ProjectionManagementClient = null;
+ }
+
+ public async Task StopTestHostForScenarioRun(object? _ = null)
{
- String result = String.Empty;
- if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("ENV_IPADDRESS")))
+ if (this.TestingContext.BackendHost != null)
{
- // Nothing in environment so not running under CI
- IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName());
- foreach (IPAddress ip in host.AddressList)
- {
- this.Trace($"{ip}");
- if (ip.AddressFamily == AddressFamily.InterNetwork)
- {
- result = ip.ToString();
- break;
- }
- }
- }
- else {
- result= Environment.GetEnvironmentVariable("ENV_IPADDRESS");
+ await this.TestingContext.BackendHost.DisposeAsync().ConfigureAwait(false);
+ this.TestingContext.BackendHost = null;
}
- return result;
+ this.HttpClient?.Dispose();
+ this.TestHostHttpClient?.Dispose();
+ this.HttpClient = null;
+ this.TestHostHttpClient = null;
}
- public String LocalIPAddress { get; private set; }
+ public string GetLocalIPAddress()
+ {
+ string? configuredAddress = Environment.GetEnvironmentVariable("ENV_IPADDRESS");
+ if (string.IsNullOrWhiteSpace(configuredAddress) == false)
+ {
+ return configuredAddress;
+ }
- public override ContainerBuilder SetupTransactionProcessorAclContainer(){
- this.AdditionalVariables.Add(ContainerType.TransactionProcessorAcl, new Dictionary {
- {"AppSettings:SkipVersionCheck", "true"}
- });
-
- return base.SetupTransactionProcessorAclContainer();
+ return "127.0.0.1";
}
- public override async Task CreateSubscriptions()
+ public string ConfigHostUrlForCurrentPlatform()
{
- List<(String streamName, String groupName, Int32 maxRetries)> subscriptions = new List<(String streamName, String groupName, Int32 maxRetries)>();
- subscriptions.AddRange(TransactionProcessor.IntegrationTesting.Helpers.SubscriptionsHelper.GetSubscriptions());
-
- foreach ((String streamName, String groupName, Int32 maxRetries) subscription in subscriptions)
+ if (AppiumDriverWrapper.MobileTestPlatform == MobileTestPlatform.Android)
{
- var x = subscription;
- x.maxRetries = 2;
- await this.CreatePersistentSubscription(x);
+ return this.TestingContext.BackendHost?.AndroidBaseUrl ?? $"http://10.0.2.2:{this.ConfigHostPort}";
}
+
+ return this.TestingContext.BackendHost?.WindowsBaseUrl ?? $"http://127.0.0.1:{this.ConfigHostPort}";
}
- String Serialise(Object arg)
+ public void ApplySeed(BackendSeed seed)
{
- return StringSerialiser.Serialise