Initialize TrezorOne mocking
ADD
WalletWasabi.Tests/HwiTests/IMockHwiProcessBridge.cs
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Interfaces;
+
+namespace WalletWasabi.Tests.HwiTests
+{
+ public class IMockHwiProcessBridge : IProcessBridge
+ {
+ public Task<(string response, int exitCode)> SendCommandAsync(string arguments, CancellationToken cancel)
+ {
+ if (arguments == "--testnet enumerate")
+ {
+ var response = "[{\"type\": \"ledger\", \"path\": \"IOService:/ AppleACPIPlatformExpert / PCI0@0 / AppleACPIPCI / XHC1@14 / XHC1@14000000 / HS02@14200000 / Nano S@14200000 / Nano S@0 / IOUSBHostHIDDevice@14200000,0\", \"serial_number\": \"0001\"}]";
+ var code = 0;
+ return Task.FromResult((response, code));
+ }
+ else
+ {
+ throw new NotImplementedException($"Mocking is not implemented for '{arguments}'");
+ }
+ }
+ }
+}

ADD
WalletWasabi.Tests/HwiTests/TrezorOneTests.cs
+using NBitcoin;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Hwi2;
+using Xunit;
+
+namespace WalletWasabi.Tests.HwiTests
+{
+ public class TrezorOneTests
+ {
+ #region SharedVariables
+
+ // Bottleneck: Windows CI.
+ public TimeSpan ReasonableRequestTimeout { get; } = TimeSpan.FromMinutes(1);
+
+ public HwiClient HwiClient { get; } = new HwiClient(Network.TestNet, new IMockHwiProcessBridge());
+
+ #endregion SharedVariables
+
+ #region Tests
+
+ [Fact]
+ public async Task CanEnumerateTestsAsync()
+ {
+ using (var cts = new CancellationTokenSource(ReasonableRequestTimeout))
+ {
+ IEnumerable<string> enumerate = await HwiClient.EnumerateAsync(cts.Token);
+ Assert.Single(enumerate);
+ }
+ }
+
+ #endregion Tests
+ }
+}

MODIFY
WalletWasabi/Hwi2/HwiClient.cs
WalletWasabi/Hwi2/HwiClient.cs
@@ -13,6 +13,8 @@ using System.Threading.Tasks;
using WalletWasabi.Helpers;
using WalletWasabi.Hwi2.Exceptions;
using WalletWasabi.Hwi2.Models;
+using WalletWasabi.Hwi2.ProcessBridge;
+using WalletWasabi.Interfaces;
namespace WalletWasabi.Hwi2
{
@@ -21,14 +23,16 @@ namespace WalletWasabi.Hwi2
#region PropertiesAndMembers
public Network Network { get; }
+ public IProcessBridge Bridge { get; }
#endregion PropertiesAndMembers
#region ConstructorsAndInitializers
- public HwiClient(Network network)
+ public HwiClient(Network network, IProcessBridge bridge = null)
{
Network = Guard.NotNull(nameof(network), network);
+ Bridge = bridge ?? new HwiProcessBridge();
}
#endregion ConstructorsAndInitializers
@@ -37,30 +41,16 @@ namespace WalletWasabi.Hwi2
private async Task<string> SendCommandAsync(IEnumerable<HwiOptions> options, HwiCommands? command, CancellationToken cancel)
{
- string hwiPath = GetHwiPath();
string arguments = GetArguments(options, command);
try
{
- using (var process = Process.Start(
- new ProcessStartInfo
- {
- FileName = hwiPath,
- Arguments = arguments,
- RedirectStandardOutput = true,
- UseShellExecute = false,
- CreateNoWindow = true,
- WindowStyle = ProcessWindowStyle.Hidden
- }
- ))
- {
- await process.WaitForExitAsync(cancel).ConfigureAwait(false);
+ (string responseString, int exitCode) = await Bridge.SendCommandAsync(arguments, cancel).ConfigureAwait(false);
- string responseString = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
- if (process.ExitCode != 0)
+ if (exitCode != 0)
{
ThrowIfError(responseString);
- throw new HwiException(HwiErrorCode.UnknownError, $"'hwi {arguments}' exited with incorrect exit code: {process.ExitCode}.");
+ throw new HwiException(HwiErrorCode.UnknownError, $"'hwi {arguments}' exited with incorrect exit code: {exitCode}.");
}
ThrowIfError(responseString);
@@ -74,7 +64,6 @@ namespace WalletWasabi.Hwi2
return responseString;
}
}
- }
catch (Exception ex) when (ex is OperationCanceledException || ex is TaskCanceledException || ex is TimeoutException)
{
throw new OperationCanceledException($"'hwi {arguments}' operation is canceled.", ex);
@@ -156,8 +145,11 @@ namespace WalletWasabi.Hwi2
{
if (JsonHelpers.TryParseJToken(responseString, out JToken responseJToken))
{
- if (responseJToken.HasValues)
+ if (responseJToken is JArray)
{
+ return;
+ }
+
var errToken = responseJToken["error"];
var codeToken = responseJToken["code"];
string err = "";
@@ -184,7 +176,6 @@ namespace WalletWasabi.Hwi2
throw new HwiException(HwiErrorCode.UnknownError, err);
}
}
- }
else
{
var subString = "error:";
@@ -197,32 +188,6 @@ namespace WalletWasabi.Hwi2
}
}
- private static string GetHwiPath()
- {
- var fullBaseDirectory = EnvironmentHelpers.GetFullBaseDirectory();
-
- string commonPartialPath = Path.Combine(fullBaseDirectory, "Hwi2", "Binaries");
- string path;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- path = Path.Combine(commonPartialPath, "hwi-win64", "hwi.exe");
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- {
- path = Path.Combine(commonPartialPath, "hwi-lin64", "hwi");
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
- {
- path = Path.Combine(commonPartialPath, "hwi-osx64", "hwi");
- }
- else
- {
- throw new NotSupportedException("Operating system is not supported.");
- }
-
- return path;
- }
-
private string GetArguments(IEnumerable<HwiOptions> options, HwiCommands? command)
{
options = options ?? Enumerable.Empty<HwiOptions>();

ADD
WalletWasabi/Hwi2/ProcessBridge/HwiMockBridge.cs
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Interfaces;
+
+namespace WalletWasabi.Hwi2.ProcessBridge
+{
+ public class HwiMockBridge : IProcessBridge
+ {
+ public Task<(string response, int exitCode)> SendCommandAsync(string arguments, CancellationToken cancel)
+ {
+ return Task.FromResult(("mock mock", 0));
+ }
+ }
+}

ADD
WalletWasabi/Hwi2/ProcessBridge/HwiProcessBridge.cs
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Helpers;
+using WalletWasabi.Interfaces;
+
+namespace WalletWasabi.Hwi2.ProcessBridge
+{
+ public class HwiProcessBridge : IProcessBridge
+ {
+ public async Task<(string response, int exitCode)> SendCommandAsync(string arguments, CancellationToken cancel)
+ {
+ string responseString;
+ int exitCode;
+ string hwiPath = GetHwiPath();
+ using (var process = Process.Start(
+ new ProcessStartInfo
+ {
+ FileName = hwiPath,
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden
+ }
+ ))
+ {
+ await process.WaitForExitAsync(cancel).ConfigureAwait(false);
+
+ exitCode = process.ExitCode;
+ responseString = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
+ }
+
+ return (responseString, exitCode);
+ }
+
+ private string GetHwiPath()
+ {
+ var fullBaseDirectory = EnvironmentHelpers.GetFullBaseDirectory();
+
+ string commonPartialPath = Path.Combine(fullBaseDirectory, "Hwi2", "Binaries");
+ string path;
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ path = Path.Combine(commonPartialPath, "hwi-win64", "hwi.exe");
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ path = Path.Combine(commonPartialPath, "hwi-lin64", "hwi");
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ path = Path.Combine(commonPartialPath, "hwi-osx64", "hwi");
+ }
+ else
+ {
+ throw new NotSupportedException("Operating system is not supported.");
+ }
+
+ return path;
+ }
+ }
+}

ADD
WalletWasabi/Interfaces/IProcessBridge.cs
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace WalletWasabi.Interfaces
+{
+ public interface IProcessBridge
+ {
+ Task<(string response, int exitCode)> SendCommandAsync(string arguments, CancellationToken cancel);
+ }
+}

MODIFY
azure-pipelines-linux.yml
azure-pipelines-linux.yml
@@ -18,4 +18,4 @@ jobs:
inputs:
command: test
projects: 'WalletWasabi.Tests/WalletWasabi.Tests.csproj'
- arguments: --configuration $(testConfiguration) --filter "CryptoTests | ExtPubKeyExplorerTests | KeyManagementTests | ModelTests | StoreTests | NoConnectedDeviceTests"
+ arguments: --configuration $(testConfiguration) --filter "CryptoTests | ExtPubKeyExplorerTests | KeyManagementTests | ModelTests | StoreTests | HwiTests"

MODIFY
azure-pipelines-osx.yml
azure-pipelines-osx.yml
@@ -18,4 +18,4 @@ jobs:
inputs:
command: test
projects: 'WalletWasabi.Tests/WalletWasabi.Tests.csproj'
- arguments: --configuration $(testConfiguration) --filter "CryptoTests | ExtPubKeyExplorerTests | KeyManagementTests | ModelTests | StoreTests | NoConnectedDeviceTests"
+ arguments: --configuration $(testConfiguration) --filter "CryptoTests | ExtPubKeyExplorerTests | KeyManagementTests | ModelTests | StoreTests | HwiTests"

MODIFY
azure-pipelines-windows.yml
azure-pipelines-windows.yml
@@ -18,4 +18,4 @@ jobs:
inputs:
command: test
projects: 'WalletWasabi.Tests/WalletWasabi.Tests.csproj'
- arguments: --configuration $(testConfiguration) --filter "CryptoTests | ExtPubKeyExplorerTests | KeyManagementTests | ModelTests | StoreTests | NoConnectedDeviceTests"
+ arguments: --configuration $(testConfiguration) --filter "CryptoTests | ExtPubKeyExplorerTests | KeyManagementTests | ModelTests | StoreTests | HwiTests"

Initialize TrezorOne mocking